How to override /Token api call behaviour in ASP.NET Web API Application - asp.net

I have an ASP.NET Web API Application and I'm trying to make my API calls secure and only available to registered users.
I know that a registered user can get an access token by invoking /Token and sending a username and password and grant_type along with a request.
I understand this all happens because of the following configuration inside Startup.Auth.cs file in App_start folder :
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
Normally calling /Token with a username and password, would search the default users table and if that user exists then it sends the access token to the user.
However in my project i have my own method/table to register users and store them in the database. So I'm not using the default users table provided by ASP.NET Identity.
Question:
Is it possible for me to somehow override/change how a request to /Token is handled? So in there I can check the username/password through my own custom users table and then send the access token if everything was ok.
Edit:
I thought of a way but I really doubt that it should be the way to go. In my own custom method for registering users, when everything is fine i can make a call to
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
// Return error
}
// Return success
And now my users are in the ASP.NET Users table as well and subsequent calls to /Token should recognize them as registered users. But I figure now I have duplicated my users in two tables. Is there any clean way to achieve what i want?

Related

Azure Active Directory SSO with MSAL and openID Connect

I was tasked with writing an ASP.NET website that uses Azure Active Directory. I went with the route of OAuth and OpenID Connect. I am not able to use implicit flow and therefore must set the ResponseType to be code.
Using MSAL code samples I got most of it working but the problem is that all the samples are using a response type that returns tokens. I think I need to do it in 2 separate steps, first get the authorization code and then get the id token. I'm not exactly sure how to do this and would much appreciate some guidance here.
I have a startup class that look like this:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
Authority = authority,
ClientId = clientId,
RedirectUri = redirectUri,
Scope = "openid profile email offline_access user.readbasic.all", // a basic set of permissions for user sign in & profile access
ResponseType = OpenIdConnectResponseType.Code,
ClientSecret = clientSecret,
TokenValidationParameters = new TokenValidationParameters
{
// In a real application you would use ValidateIssuer = true for additional checks and security.
ValidateIssuer = false,
NameClaimType = "name",
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
}
});
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
// Handle any unexpected errors during sign in
context.OwinContext.Response.Redirect("/Error?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
/*
The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
Azure AD and has a full set of claims.
*/
IConfidentialClientApplication confidentialClient = GroupManager.Utils.MsalAppBuilder.BuildConfidentialClientApplication(null);
// Upon successful sign in, get & cache a token using MSAL
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(new[] { "openid profile email offline_access user.readbasic.all" }, context.Code).ExecuteAsync();
}
How do I take the information from the result's tokens and create a claims identity for the AuthenticationTicket.Identity and access the user info?
Please note that this is an ASP.NET application. Not MVC and not Core.
If you use MSAL, you don't need to handle the code yourself. MSAL will return the token to you after you log in interactively, please see:Overview of Microsoft Authentication Library (MSAL).
Before that, you need to take a look at Add sign-in to Microsoft to an ASP.NET web app,the workflow is:
Code example please check: https://github.com/AzureAdQuickstarts/AppModelv2-WebApp-OpenIDConnect-DotNet
Update:
Try to enable ID token

Microsoft Graph SDK - Get drives as authed app (Not user)

Im having some problems retriving data from sharepoint (Disks) for a dotnet core app.
At the moment my app tries to use the app itself, and not the logged in user to retrive disks, but the prefered way would be to use the accesstoken for the logged in user instead.
Maybe authenticating as the app with clientId and secret wont work with drives at all?
The login works fine.
I've set up a dotnet core app with the following startup:
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30);
})
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
I also have the following services registered:
services.AddTransient<IAuthenticationProvider, GraphAuthenticationProvider>();
services.AddTransient<IGraphServiceClient, GraphServiceClient>();
services.AddTransient<IGraphProvider, MicrosoftGraphProvider>();
where i use the this to authenticate:
public class GraphAuthenticationProvider : IAuthenticationProvider
{
public const string GRAPH_URI = "https://graph.microsoft.com/";
private string _tenantId { get; set; }
private string _clientId { get; set; }
private string _clientSecret { get; set; }
public GraphAuthenticationProvider(IConfiguration configuration)
{
_tenantId = configuration.GetValue<string>("AzureAd:TenantId");
_clientId = configuration.GetValue<string>("AzureAd:ClientId");
_clientSecret = configuration.GetValue<string>("AzureAd:ClientSecret");
}
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{_tenantId}");
ClientCredential creds = new ClientCredential(_clientId, _clientSecret);
//I have tried using acquireTokensAsync with scopes, but there is no such method.
AuthenticationResult authResult = await authContext.AcquireTokenAsync(GRAPH_URI, creds);
request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
}
}
I have given the app plenty of permissions in the API settings in portal, mostly because im unsure what i need, and at the moment im just eager to make it work first, then refactor some.
The app is able to log in, and retrive the following data with the SDK:
var groups = await _graphServiceClient.Groups[appSettings.AzureAd.GroupId].Request().GetAsync();
however: the following does not work:
var groupDrives = await _graphServiceClient.Groups[appSettings.AzureAd.GroupId].Drives
.Request()
.GetAsync();
and i get the following error:
Code: AccessDenied
Message: Either scp or roles claim need to be present in the token.
I also have user login in startup, and the app wont be used without logging in towards azure AD:
Could i use the accessToken for the user instead?
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
options.TokenValidationParameters.ValidateIssuer = false;
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async ctx =>
{
var roleGroups = new Dictionary<string, string>();
Configuration.Bind("AuthorizationGroups", roleGroups);
var clientApp = ConfidentialClientApplicationBuilder
.Create(Configuration["AzureAD:ClientId"])
.WithTenantId(Configuration["AzureAD:TenantId"])
.WithClientSecret(Configuration["AzureAD:ClientSecret"])
.Build();
var authResult = await clientApp
.AcquireTokenOnBehalfOf(new[] { "User.Read", "Group.Read.All" }, new UserAssertion(ctx.SecurityToken.RawData))
.ExecuteAsync();
var graphClient = new GraphServiceClient(
"https://graph.microsoft.com/v1.0",
new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", authResult.AccessToken);
}));
//Could i register the graphservice as a singelton with the users accesstoken?
//Fetching drives here with the accessToken from user works.
var graphService = new GraphService(graphClient, Configuration);
var memberGroups = await graphService.CheckMemberGroupsAsync(roleGroups.Keys);
var claims = memberGroups.Select(groupGuid => new Claim(ClaimTypes.Role, roleGroups[groupGuid]));
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
};
});
I would actually like to use the users accesstoken to retrive the drives etc, but im not sure on how to store\reuse the accesstoken. I should probably register the service as a singelton with the users accesstoken as mentioned in the comment?
I followed this guide, and it has the same classes\services i have used:
http://www.keithmsmith.com/get-started-microsoft-graph-api-calls-net-core-3/
I actually thought the option on top here was just a header. It might be easier now.. https://i.imgur.com/yfZWaoe.png
it feels like you are mixing up a whole bunch of concepts here. that example you are using is based on the client credentials flow. you should probably start by reading up on the different types of authentication flows available. https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows
In general when you use the client credential flow, the permissions you need to set are application permissions in the api permissions blade. Delegated permissions are for user login flows.
when you are using delegated permissions like you are above. and you use a flow that gets user tokens, then the access that the application has is based on the access the user has. for example, if you delegate groups.read.all with delegated permissions, then that gives the application access to read all the groups that That specific user has access to. it doesn't give the application access to all groups. if this is what you want, then by all means use the user flow.
You didn't mention if you were writing a web app, or what, but if you are you may want to look carefully at the on-behalf-of flow. here is an example of it. https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/2-WebApp-graph-user/2-1-Call-MSGraph
but again above applies for the permissions, when you get a user token your app will only have access to the items that user has access to. no more. eg user A has access to sharepoint site A, user B has no access to site A, when you use a user token for user B to call graph it will not return results for site A since user B does not have access to it.
You've defined Delegated scopes but are attempting to authenticate using Client Credentials. Delegated scopes are named such because the User is delegating their access to your application.
You need to request Application scopes when authenticating without a User.

Kentor AuthServices/Owin - handling the response from the identity provider

I have a working configuration to authenticate against Azure AD using KentorAuthServices & Owin, but I need to know some basic information about the user that has just logged in. When I used WSFed as the authentication service I could simply handle the SecurityTokenValidated notification as per below. How do I do similarly with KentorAuthServices? I don't see an appropriate notification to pull this information. All I need is the username/email address the user logged in with.
Notifications = new WsFederationAuthenticationNotifications
{
SecurityTokenValidated = context =>
{
string username = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(60), true, "");
String encryptedTicket = FormsAuthentication.Encrypt(authTicket);
context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);
return Task.FromResult(0);
}
}
ETA: Doing some more digging I believe AcsCommandResultCreated is the notification that I want to hook into - but this never fires?
All the information in the SAML Response is converted to claims in the resulting identity. If you are using the default template with ASP.Net Identity you can access the external identity in the ExternalLoginCallback action on the AccountController.
The AcsCommandResultCreated notification should definitely be fired on a sucessful login. Try enable the Katana logging and see if the login sequence is aborted due to an error.

IdentityServer Hybrid Flow - Access Token is null after user successful login

I'm having problems in retrieving access token of an authenticated user. below is my configuration
ASP.NET MVC 5 Client:
OpenIdConnect
IdentityServer3 libraries
ResponseType = "code id_token"
ASP.NET Core Identity Server:
IdentityServer4 libraries
Client Config: AllowedGrantTypes =
GrantTypes.HybridAndClientCredentials,
I'm trying to get the access token in my client using this:
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(TokenEndpoint, "clientid", "secret");
var response = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
},
I used this reference for above implementation - https://github.com/IdentityServer/IdentityServer3/issues/2457
but the properties in the response has null values. I need the access token so that the user logged in the client can access the api. Below is another way that i'm trying to retrieve the access token:
public async Task<ActionResult> CallApiUsingUserAccessToken()
{
var user = User as ClaimsPrincipal;
var accessToken = user.FindFirst("access_token").Value;
var client = new HttpClient();
client.SetBearerToken(accessToken);
var content = await client.GetStringAsync("http://localhost:6001/api/values");
ViewBag.Json = JArray.Parse(content).ToString();
return View("json");
}
however, user.FindFirst("access_token").Value; is null. I'm thinking of migrating my MVC client to Core because I've tried the IdentityServer4 version in an asp.net core but that seems to be a big migration to my part. Thank you.
[updated]
It never occured to me that the endpoints in the IdentityServer3 differs from IDS4. I did have to change var tokenClient = new TokenClient(TokenEndpoint, "client", "secret"); to var tokenClient = new TokenClient("http://localhost:9000/connect/token", "client", "secret") since TokenEndpoint in IDS3 is http://localhost:9000/core/connect/token which the endpoint "core" does not exist in IDS4. I'm able to get the access token in this line var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri); but after authorization, i'm still getting nullreference exception to this var accessToken = user.FindFirst("access_token").Value; line of code.
Given the IdentityServer 4 documentation on
Switching to Hybrid Flow and adding API Access back
and an example client from IdentityServer3.Samples
MVC OWIN Client (Hybrid)
you should be able to setup a working environment.
To support debugging you should always do proper response handling as shown in example below and copied from example client. Add any response errors to your question.
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
Constants.TokenEndpoint,
"mvc.owin.hybrid",
"secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
Finally I recommend to add code for all important parts of an IdentityServer3/4 based setup - because the truth is usually burried in the details.
According to these posts, https://github.com/IdentityServer/IdentityServer3/issues/2457 & https://github.com/IdentityServer/IdentityServer3/issues/2015#issuecomment-172623173, it is a good practice to not include the access token in the claims. Hence, I followed his example, https://github.com/Mich-b/IdentityServerTMLClient/blob/master/IdentityServerTMLClient/Startup.cs, in which the access token is added in the Http Session storage.

asp.net core identity extract and save external login tokens and add claims to local identity

I am a stackoverflow noob so please go easy if I am doing this wrong.
I am using asp.net core with the default core identity template (local accounts).
I have accertained how to add claims to user principal when they login locally like so
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var user = await _userManager.FindByNameAsync(model.Email);
await _userManager.AddClaimAsync(user, new Claim("your-claim", "your-value"));
And I have figured out how to get claims returned from the external login but I cannot figure out how I would add these before the user principal gets created in the ExternalLoginCallback function
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
return View(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
else {
// extract claims from external token here
}
// assume add claims to user here before cookie gets created??
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
I am assuming the the _signInManager.ExternalLoginSignInAsync function works similar to the local login _signInManager.PasswordSignInAsync in the sense that once it is called, the cookie will be created. But I am just not sure.
Essentially what I am hoping to achieve, is understanding of how to add custom claims into the cookie that gets created regardless of how to user logins in (local or external), and how to persist these claims to the database if required.
I am planning on doing some work where if I have a user login using say google auth, I need to save that access_token from google, because I wish to call into the Google APIs later with it. So I need to be able to include this access_token in with the User Principal that gets created, and I would hope the cookie would have a claim on it I could use at the front end as well.
This might be out of scope on this question but I would also like when the google token expires, for some-how it to use the refresh token and go get a new one, or force the user to relogin.
Any help on this would be super appreciated, I have really tried hard to understand this without posting this question to stackoverflow. I have read many articles with lots of useful info, but does not provide the answers this specific question is asking. So Thank you very much in advance.
cheers
When you use await _userManager.AddClaimAsync(user, new Claim("your-claim", "your-value")); that actually updates the Identity's aspnetuserclaims table.
Whenever you sign in (by using _signInManager.PasswordSignIn or _signInManager.ExternalLoginSignInAsync) the claims from that table are read and added to the cookie that on every request becomes the Principal.
So you probably don't want to be calling the AddClaimAsync method from UserManager on every login.
Regarding external login providers, you have access to the claims when you call (in ExternalCallback and ExternalCallbackConfirmation if you are using the default templates) here:
var info = await _signInManager.GetExternalLoginInfoAsync();
The claims are in info.Principal.Claims.
The access token is not included by default. When it is, it will be here (along with the type and expiry date):
var accessToken = info.AuthenticationTokens.Single(f => f.Name == "access_token").Value;
var tokenType = info.AuthenticationTokens.Single(f => f.Name == "token_type").Value;
var expiryDate = info.AuthenticationTokens.Single(f => f.Name == "expires_at").Value;
To have the access token be included in the AuthenticationTokens collection, when you are configuring the GoogleAuthentication middleware set the SaveTokens flag to true:
app.UseGoogleAuthentication(new GoogleOptions{
ClientId = "...",
ClientSecret = "...",
SaveTokens = true
Now, if you want to have control over which claims go in the cookie you have to "take over" the process of creating the claims principal.
This is done for you when you use _signInManager.PasswordSignIn/ExternalLoginSignInAsync.
So, for example, for ExternalLoginSignInAsync replace:
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
With:
var user = await this._userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
var claimsPrincipal = await this._signInManager.CreateUserPrincipalAsync(user);
((ClaimsIdentity)claimsPrincipal.Identity).AddClaim(new Claim("accessToken", info.AuthenticationTokens.Single(t => t.Name == "access_token").Value));
await HttpContext.Authentication.SignInAsync("Identity.Application", claimsPrincipal);
"Identity.Application" is the default cookie name. You can change it in Startup's ConfigureServices method, for example to MainCookie:
services.Configure<IdentityOptions>(options => {
options.Cookies.ApplicationCookie.AuthenticationScheme = "MainCookie";
});
You still need to handle the ExternalCallbackConfirmation action in the AccountController. It will be similar to the example above.

Resources