IdentityServer4 as external provider, how to avoid logout prompt? - c#

I am working with two identity providers, both implemented using IdentityServer4 in ASP.NET MVC Core 2.2. One of them is used as an external provider by the other. Let's call them "primary" and "external". The primary provider is referenced directly by the web application. The external provider is an optional login method provided by the primary provider.
The web application uses the oidc-client-js library to implement authentication. The logout operation in the web app calls UserManager.signoutRedirect. This works fine when the primary identity provider is used (no logout confirmation prompt is shown). However, when the external provider is used, the user is prompted to sign out from the external provider.
The sequence of requests when logging out are:
GET http://{primary}/connect/endsession?id_token_hint=...&post_logout_redirect_uri=http://{webapp}
GET http://{primary}/Account/Logout?logoutId=...
GET http://{external}/connect/endsession?state=...&post_logout_redirect_uri=http://{primary}/signout-callback-{idp}&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
GET http://{external}/Account/Logout?logoutId=...
This last request above shows the logout confirmation screen from the external provider.
The code for the /Account/Logout page on the primary provider is almost identical to the sample code in the documentation:
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
var vm = await BuildLogoutViewModelAsync(logoutId);
if (!vm.ShowLogoutPrompt)
{
// If the request is authenticated don't show the prompt,
// just log the user out by calling the POST handler directly.
return Logout(vm);
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated)
{
// delete local authentication cookie
await _signInManager.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
var ap = new AuthenticationProperties { RedirectUri = url };
return SignOut(ap, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
The BuildLogoutViewModelAsync method calls GetLogoutContextAsync to check if the logout is authenticated, like so:
public async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel
{
LogoutId = logoutId,
ShowLogoutPrompt = true
};
var context = await _interaction.GetLogoutContextAsync(logoutId);
if (context?.ShowSignoutPrompt == false)
{
// It's safe to automatically sign-out
vm.ShowLogoutPrompt = false;
}
return vm;
}
The BuildLoggedOutViewModelAsync method basically just checks for an external identity provider and sets the TriggerExternalSignout property if one was used.
I hate to make this a wall of code, but I'll include the ConfigureServices code used to configure the primary identity server because it is probably relevant:
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme;
opts.Authority = openIdConfig.ProviderAuthority;
opts.ClientId = openIdConfig.ClientId;
opts.ClientSecret = openIdConfig.ClientSecret;
opts.ResponseType = "code id_token";
opts.RequireHttpsMetadata = false;
opts.CallbackPath = $"/signin-{openIdConfig.Scheme}";
opts.SignedOutCallbackPath = $"/signout-callback-{openIdConfig.Scheme}";
opts.RemoteSignOutPath = $"/signout-{openIdConfig.Scheme}";
opts.Scope.Clear();
opts.Scope.Add("openid");
opts.Scope.Add("profile");
opts.Scope.Add("email");
opts.Scope.Add("phone");
opts.Scope.Add("roles");
opts.SaveTokens = true;
opts.GetClaimsFromUserInfoEndpoint = true;
var mapAdditionalClaims = new[] { JwtClaimTypes.Role, ... };
foreach (string additionalClaim in mapAdditionalClaims)
{
opts.ClaimActions.MapJsonKey(additionalClaim, additionalClaim);
}
opts.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
}
My understanding is that the id_token_hint parameter passed to the first /connect/endsession endpoint will "authenticate" the logout request, which allows us to bypass the prompt based on the ShowSignoutPrompt property returned by GetLogoutContextAsync. However, this does not happen when the user is redirected to the external provider. The call to SignOut generates the second /connect/endsession URL with a state parameter, but no id_token_hint.
The logout code in the external provider is basically the same as the code shown above. When it calls GetLogoutContextAsync, that method does not see the request as authenticated, so the ShowSignoutPrompt property is true.
Any idea how to authenticate the request to the external provider?

The final block of code, you hate, but luckily added, contains one significant row:
opts.SaveTokens = true;
That allows you later to restore the id_token you got from the external provider.Then you can use it as a "second level hint".
if (vm.TriggerExternalSignout)
{
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
var props = new AuthenticationProperties {RedirectUri = url};
props.SetParameter("id_token_hint", HttpContext.GetTokenAsync("id_token"));
return SignOut(props, vm.ExternalAuthenticationScheme);
}

I had the exact same problem as OP and was able to correct it by explicitly stating that the ID Token is to be added on to the logout request as per this Github Issue
https://github.com/IdentityServer/IdentityServer4/issues/3510
options.SaveTokens = true; // required for single sign out
options.Events = new OpenIdConnectEvents // required for single sign out
{
OnRedirectToIdentityProviderForSignOut = async (context) => context.ProtocolMessage.IdTokenHint = await context.HttpContext.GetTokenAsync("id_token")
};

I have come up with a solution, though it seems to contradict what is done in the samples.
The problem seems to be caused by two lines of code that were both from the IdentityServer samples that we used as a basis for our IDP implementations. The problem code is in the "primary" IDP.
The first line is in ConfigureServices in Startup.cs:
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme; // this is a problem
The second place is in ExternalController.cs, in the Callback method. Here we diverged from the samples, using IdentityServerConstants.ExternalCookieAuthenticationScheme instead of IdentityConstants.ExternalScheme:
// Read external identity from the temporary cookie
var result = await this.HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// ...
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme); // this is a problem
What happens at logout is: since the SignOutScheme is overridden, it is looking for a cookie that doesn't exist. Simply removing that doesn't fix it because the call to SignOutAsync has deleted the cookie that contains the information required for the identity code to authenticate the scheme. Since it can't authenticate the scheme, it does not include the id_token_hint in the request to the "external" IDP.
I've been able to fix this by removing the code that overrides SignOutScheme in Startup.cs, and moving the code that deletes the ExternalCookieAuthenticationScheme cookie to the Logout endpoint in AccountController.cs:
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// delete temporary cookie used during external authentication
await this.HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// build a return URL so the upstream provider will redirect back...
This way the "temporary" external cookie is left around until it is needed, but is deleted when the user logs out.
I'm not sure if this is the "correct" solution, but it does seem to work correctly in all cases that I've tested. I'm not really sure why we deviated from the sample in ExternalController.cs, either, but I suspect it is because we have two standalone IDP rather than a site with a single standalone IDP. Also, the sample appears to be using implicit flow while we are using hybrid flow.

Related

How to retrieve AuthenticationToken while processing API request?

I've configured External provider authentication to my Blazor WASM app. User can choose to log in via Spotify account and after that, I want my server to download some data about him from Spotify API.
services.AddAuthentication()
.AddIdentityServerJwt()
.AddSpotify(options =>
{
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.CallbackPath = "/signin-spotify";
options.SaveTokens = true;
var scopes = new List<string> {
//scopes
};
options.Scope.Add(string.Join(",", scopes));
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
ctx.Properties.IsPersistent = true;
return Task.CompletedTask;
};
});
In order to call Spotify API, I need an access token. Token is, if I understand correctly, given to my server after user logs in. In above code, I've specified OnCreatingTicket event and I can see it is being fired (just after I log in) and access_token is in tokens list.
Now, the problem is, I don't know how to retrieve that token later.
Here is what happens after log in:
User navigates to \LikedSongs (blazor wasm subpage that is meant to display data)
Blazor page calls my server's API to retrieve data that will be later displayed
protected override async Task OnInitializedAsync()
{
savedTracks = await HttpClient.GetFromJsonAsync<SavedTrack[]>("UsersTracks");
}
Finally, my API controller is being fired:
[HttpGet]
public async Task<IEnumerable<SavedTrack>> GetAsync()
{
// here I need to have access_token
// ASP.net MVC tutorial I follow states, that below line should work
var token = await _httpContextAccessor.HttpContext.GetTokenAsync("Spotify", "access_token");
// unfortunately token == null
}
For some reason, token is null. And I can't find any other tokens in HttpContext. As I understand correctly, tokens are encoded in cookies, so why I can't find any of them there?

How to redirect user to client app after logging out from identity server?

I want to redirect user to the same client after he logged out from that client. So if i have lets say 5 clients on one identity server, i want users to be able to log out from one client and be on that same client but logged out.
The one thing i have tried is to use PostLogoutRedirectUri in AccountController in quickstart, but the value is always null. Workaround that i found is to manually set PostLogoutRedirectUri, that works fine if you have only one client on the server, but not so much if I have multiple. Is there any way to know which client has been "logged out"?
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
vm.PostLogoutRedirectUri = "http://localhost:56582";
return Redirect(vm.PostLogoutRedirectUri);
}
My Client
new Client
{
ClientId = "openIdConnectClient",
ClientName = "Implicit Client Application Name",
AllowedGrantTypes = GrantTypes.Implicit,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"role",
"customAPI.write"
},
RedirectUris = new List<string>{ "http://localhost:56582/signin-oidc" },
PostLogoutRedirectUris = new List<string>{ "http://localhost:56582" },
// FrontChannelLogoutUri = "http://localhost:56582/signout-oidc"
}
You are not supposed to set the uri manually. Actually the default logout method from the IdentityServer samples works fine.
When you try the 3_ImplicitFlowAuthentication sample project, you'll see PostLogoutRedirectUri is not null and the redirection works (but not automatically).
The reason why PostLogoutRedirectUri is null in your case is probably because the id_token is not preserved. In MvcClient.Startup make sure you add this line:
options.SaveTokens = true;
That will preserve the tokens in a cookie.
In order to automatically redirect back to the client, make a few adjustments to the sample code. In IdentityServer AccountOptions set
AutomaticRedirectAfterSignOut = true;
In the AccountController.Logout method add the following lines:
if (vm.AutomaticRedirectAfterSignOut &&
!string.IsNullOrWhiteSpace(vm.PostLogoutRedirectUri))
return Redirect(vm.PostLogoutRedirectUri);
Just before the last line:
return View("LoggedOut", vm);
When you run the sample again you should see that the user is now automatically returned to the client after logout.

Asp.Net core 2 Round trip external login additional parameter

Currently, I generate an invitation key that gets embedded into the login URL and sent to users:
eg: https://localhost:44338/account/login/mykey
I also enabled google authentication
How do round trip "mykey" when the user clicks on the google link to have it available when the user gets created in ExternalLoginConfirmation (in the default asp.net core setup).
EDIT: For example what code should I add to this method
public IActionResult ExternalLogin(string provider, string key, string returnUrl = null)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
This is actually more of an OAuth question than something specific to ASP.NET. It is done by encoding a complex object and passing it to the OAuth provider in the state query paremeter.
Take for example this JSON object:
{
"validation": "aGUID",
"key": "invitationKey"
}
If you Url Encode this value, you can pass it as the state:
...&state=%7B%22validation%22%3A+%22aGUID%22%2C%22key%22%3A+%22invitationKey%22%7D+
When you receieve the response from the provider, you decode the state value and deserialize it back into an object. You can then validate the GUID in validation matches what you sent and process the invitation key from the key property.
Marc LaFleur's answer is correct, but as your follow-up question notes, you're trying to figure out how to do this with ASP.NET Core. The solution is to wire up some of the OpenIdConnectEvents which allows you to intercept various parts of the OIDC flow. This goes in your setup method where you register for OIDC:
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = appconfig["OidcAuthority"];
options.ClientId = appconfig["OidcClientId"];
options.ClientSecret = appconfig["OidcClientSecret"];
// other config omitted
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.Properties.Items.Add("invitationkey", "somevalue");
return Task.FromResult<object>(null);
},
OnTicketReceived = context =>
{
var invitation = context.Properties.Items["invitationkey"];
return Task.FromResult<object>(null);
}
}
};
There are quite a few events available. One of my sytems allows our users to auth via Google, but we use it through IdentityServer4, so offhand I don't know the specific response event. You'll have to review them to figure out which is appropriate for the type of flow you're using and how your identity provider responds, but this should get you on the right track.
Since the return-trip event is defined in startup, you'll have to come up with a way to "share" that value to other parts of your application (inject some type of simple caching service, for example).

ASP.NET Core authenticating with Azure Active Directory and persisting custom Claims across requests

I have a default ASP.NET Core website created within Visual Studio 2017. I have chosen to authenticate using an Azure Active Directory.
I run the site and can successfully login using an account in the Active Directory.
I can retrieve Claim information provided by Active Directory, e.g. by calling the following line I get the 'name'.
User.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
I want to add a custom claim - CompanyId = 123456 for the logged in user.
I'm able to add a custom claim however it is only available on the page where the claim is set.
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
((ClaimsIdentity)User.Identity).AddClaim(claim);
My understanding is that I somehow need to update the token that has been issued by Active Directory or set the claim before the token is issued. I'm unsure how to do this.
I suspect this needs to be done in the AccountController at SignIn()
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
}
I've read numerous articles and samples about this scenario (including https://github.com/ahelland/AADGuide-CodeSamples/tree/master/ClaimsWebApp) however have not managed to solve how to persist the Claim across requests.
I have successfully managed to persist custom Claims using ASP.NET Identity as the Authentication Provider, but this appears to be because the custom Claim is saved to the database..
OnTokenValidated offers you the chance to modify the ClaimsIdentity obtained from the incoming token , code below is for your reference :
private Task TokenValidated(TokenValidatedContext context)
{
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
(context.Ticket.Principal.Identity as ClaimsIdentity).AddClaim(claim);
return Task.FromResult(0);
}
Setting the OpenIdConnectEvents:
Events = new OpenIdConnectEvents
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
OnTokenValidated = TokenValidated
}
Then in controller using :
var companyId= User.Claims.FirstOrDefault(c => c.Type == "CompanyId")?.Value;
For those who would like more detail, the code provided is placed in Startup.cs
In the Configure method add/edit:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
Events = new OpenIdConnectEvents
{
OnTokenValidated = TokenValidated
}
});
The private Task TokenValidated method is in the body of Startup.cs
The following sample is a good reference.
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore-v2/blob/master/WebApp-OpenIDConnect-DotNet/Startup.cs

Return JWT Token generated by OAuthAuthorizatioServer from controller in Web API

Following #Taiseer Joudeh I was able to create simple POC of Web API. I'm able to create new account, then log-in and call secure Web API when I add JWT token to header.
I'd like to modify method that is responsible for creating accounts.
Right now I'm returning Create (201) code with new user object, but instead I'd like to return access token.
I've found similar question but it requires creating HttpClient and doing request to OAuthAuthorizatioServer TokenEndpointPath.
Second question I found requires generating temporary token that is returned to front-end, but then front-end must do additional request to server to get "real" token.
What I'd like to do is to return login response (access_token, token_type and expires_in) when I create user account.
I want user to be authenticated when his account is created.
I'm using just Web API and JWT without any cookies.
EDIT: My temporary solution:
after creating user I'm doing this:
var validTime = new TimeSpan(0, 0, 0, 10);
var identity = await UserManager.CreateIdentityAsync(user, "JWT");
var jwtFormat = new CustomJwtFormat(ApplicationConfiguration.Issuer);
var authenticationProperties = new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow, ExpiresUtc = DateTimeOffset.UtcNow.Add(validTime) };
var authenticationTicket = new AuthenticationTicket(identity, authenticationProperties);
var token = jwtFormat.Protect(authenticationTicket);
var response = new
{
access_token = token,
token_type = "bearer",
expires_in = validTime.TotalSeconds.ToInt()
};
return Ok(response);
where CustomJwtFormat comes from this awesome article.
Below is some code similar to what I'm doing in my application, which is using Asp.Net Core 1.0. Your signin and user registration will differ if you're not using Core 1.0.
public async Task<string> CreateUser(string username, string password)
{
string jwt = String.Empty;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
var user = await _userManager.FindByNameAsync(username);
if (user == null) // user doesn't exist, create user
{
var newUser = await _userManager.CreateAsync(new ApplicationUser() { UserName = username }, password);
if (newUser.Succeeded) //user was successfully created, sign in user
{
user = await _userManager.FindByNameAsync(username);
var signInResult = await _signInManager.PasswordSignInAsync(user, password, false, true);
if (signInResult.Succeeded) //user signed in, create a JWT
{
var tokenHandler = new JwtSecurityTokenHandler();
List<Claim> userClaims = new List<Claim>();
//add any claims to the userClaims collection that you want to be part of the JWT
//...
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "TokenAuth"), userClaims);
DateTime expires = DateTime.Now.AddMinutes(30); //or whatever
var securityToken = tokenHandler.CreateToken(
issuer: _tokenOptions.Issuer, //_tokenAuthOptions is a class that holds the issuer, audience, and RSA security key
audience: _tokenOptions.Audience,
subject: identity,
notBefore: DateTime.Now,
expires: expires,
signingCredentials: _tokenOptions.SigningCredentials
);
jwt = tokenHandler.WriteToken(securityToken);
Response.StatusCode = (int)HttpStatusCode.Created;
await _signInManager.SignOutAsync(); //sign the user out, which deletes the cookie that gets added if you are using Identity. It's not needed as security is based on the JWT
}
}
//handle other cases...
}
}
Basically, the user is created and then signed in automatically. I then build a JWT (add in any claims you want) and return it in the response body. On the client side (MVC and Angular JS) I get the JWT out of the response body and store it. It is then passed back to the server in the Authorization header of each subsequent request. Authorization policies for all server actions are based on the set of claims supplied by the JWT. No cookies, no state on the server.
EDIT: I suppose you don't even need to call the signIn method in this process as you can just create the user, create a JWT, and return the JWT. When a user logs in on a future request you would use the Sign-In, create token, Sign-Out approach.
The idea of sending a response with access_token, token_type and expires_in, when the user is created is a great idea. But, I would stick to calling the "/token" end point with HttpClient to achieve this task. There are quite a few security checks that needs to be performed before token is generated. Since this is security I would not take any risk. I feel comfortable using the libraries/ code provided by industry experts.
That said, I tried to come up with a class you can call to create the token once the user is created. This code has been taken from the Microsoft's implementation of OAuth Authorization Server in their Katana project. You can access the source here. As you can see there is quite a lot happening when creating the token.
Here is the modified version of that middleware class for generating the token. You have to provide the proper OAuthAuthorizationServerOptions, Context, username, password, Scopes and clientid to get an access token. Please note that this is a sample implementation to guide you in the right direction. Please test it thoroughly if you want to use this.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AspNetIdentity.WebApi.Providers;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
namespace WebApi.AccessToken
{
public class TokenGenerator
{
public string ClientId { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public IList<string> Scope { get; set; }
private OAuthAuthorizationServerOptions Options { get; } =
new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("http://localhost:59822")
};
public async Task<IList<KeyValuePair<string, string>>> InvokeTokenEndpointAsync(IOwinContext owinContext)
{
var result = new List<KeyValuePair<string, string>>();
DateTimeOffset currentUtc = Options.SystemClock.UtcNow;
// remove milliseconds in case they don't round-trip
currentUtc = currentUtc.Subtract(TimeSpan.FromMilliseconds(currentUtc.Millisecond));
AuthenticationTicket ticket = await InvokeTokenEndpointResourceOwnerPasswordCredentialsGrantAsync(owinContext, Options, currentUtc);
if (ticket == null)
{
result.Add(new KeyValuePair<string, string>("ERROR", "Failed to create acess_token"));
return result;
}
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(Options.AccessTokenExpireTimeSpan);
ticket = new AuthenticationTicket(ticket.Identity, ticket.Properties);
var accessTokenContext = new AuthenticationTokenCreateContext(
owinContext,
Options.AccessTokenFormat,
ticket);
await Options.AccessTokenProvider.CreateAsync(accessTokenContext);
string accessToken = accessTokenContext.Token;
if (string.IsNullOrEmpty(accessToken))
{
accessToken = accessTokenContext.SerializeTicket();
}
DateTimeOffset? accessTokenExpiresUtc = ticket.Properties.ExpiresUtc;
result.Add(new KeyValuePair<string, string>("access_token", accessToken));
result.Add(new KeyValuePair<string, string>("token_type", "bearer"));
TimeSpan? expiresTimeSpan = accessTokenExpiresUtc - currentUtc;
var expiresIn = (long)expiresTimeSpan.Value.TotalSeconds;
if (expiresIn > 0)
{
result.Add(new KeyValuePair<string, string>("expires_in", "bearer"));
}
return result;
}
private async Task<AuthenticationTicket> InvokeTokenEndpointResourceOwnerPasswordCredentialsGrantAsync(IOwinContext owinContext, OAuthAuthorizationServerOptions options, DateTimeOffset currentUtc)
{
var grantContext = new OAuthGrantResourceOwnerCredentialsContext(
owinContext,
options,
ClientId,
UserName,
Password,
Scope);
await options.Provider.GrantResourceOwnerCredentials(grantContext);
return grantContext.Ticket;
}
}
}
Please let me know if you have any questions.
Thank you,
Soma.
I assume you are referring to the following article: http://bitoftech.net/2015/02/16/implement-oauth-json-web-tokens-authentication-in-asp-net-web-api-and-identity-2/
The general approach to authenticating a user in this case is
Make an HTTP call to the token endpoint
User Enters the credentials at the UI that is rendered
IdP verifies the credentials and issues a token
User makes another call to an authorized endpoint and OWIN will validate this (JWT) token (as configured in the ConfigureOAuthTokenConsumption method) and if successfull will set a user session with expiry same as the token expiry. The session is set using session cookies.
Now try to understand that in general, this whole process of authentication is required because your server does not trust the user logging in to be the user the person is claiming to be. However, in your case, you (or your server code) just created a user and you know for sure that the person accessing your website is the user you have just created. In this case you don't need to validate a token to create a session for this user. Just create a session with an expiry that suits your use-case.
The next time the user will have to log-in and prove him/herself to the server using a token but this time the user does not need to prove him/her self.
Note: If you absolutely are adamant about requiring a token to log in a user who you yourself have just created using their credentials here are a couple of issues.
You are taking on the responsibility of storing (having access to) the user credentials, which might not be with you over the lifetime of the application (in most cases you might want to act as a relying party rather than an IdP).
Even if you want to then doing it is not trivial. You will have to make the calls to the token end point in code (server or client side) on behalf of the user, enter their credentials for them, retrieve the token, call an authenticated endpoint on your site and retrieve the session cookie all while hiding all this from the user, which probably is something you will either do if you hate yourself :) but also isn't very secure way of doing things especially when you are taking all the trouble to implement OAuth in the first place.
Also, take a look at Windows Server 2016 (technical preview 5 at this time) which supports implicit grants and might take writing all this custom code off your plate if you can wait a bit for RTM.
In an OAuth solution you as a developer are are not required to handle the cookie setting yourself. The cookie handling is done for you automatically by the framework.
Also the only way to set a session is a. Using session cookies or b. Use cookie-less (in the url) methods. Look at http://www.cloudidentity.com/blog/2015/02/19/introducing-adal-js-v1/ for more details on token validation and session establishment (also search the term cookie and you will know what all it's used for).
If you start thinking about not using cookies at all not only will you have to figure out how to maintain session and do it securely without cookies but also have to re-write the token refresh code that detects and refreshes the token for you based on the presence of a session cookie. (i.e. not a smart idea)
I am using the exact technology stack and recently implemented token based authorization successfully. The link I took reference from had very neatly defined the token-based auth in Web APIs. A must bookmark page I must say. Here is the link: TOKEN BASED AUTHENTICATION IN WEB APIs.

Categories