Storing claims on cookie while redirecting to other URL and also without identity authentication - asp.net-core

I just need advise if this is feasible. I am developing an authorization for my Shopify app and I need to somewhat store the access token from shopify auth for future verification of my front-end app.
So the first end-point the shopify is calling is this one:
[HttpGet("install")]
public async Task<IActionResult> Install()
{
try
{
if (ModelState.IsValid)
{
var queryString = Request.QueryString.Value;
var isValid = _shopifyService.VerifyRequest(queryString);
if (isValid)
{
var shopifyUrl = Request.Query["shop"];
var authUrl = _shopifyService.BuildAuthUrl(shopifyUrl,
$"{Request.Scheme}://{Request.Host.Value}/api/shopify/authorize",
Program.Settings.Shopify.AuthorizationScope);
return Redirect(authUrl);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
This works fine and the result of this api call will actually redirect to same link to my api, but this one will authorize the app:
[HttpGet("authorize")]
public async Task<IActionResult> AuthorizeStore()
{
try
{
if (ModelState.IsValid)
{
var code = Request.Query["code"];
var shopifyUrl = Request.Query["shop"];
var accessToken = await _shopifyService.AuthorizeStore(code, shopifyUrl).ConfigureAwait(false);
var identity = User.Identity as ClaimsIdentity;
identity.AddClaim(new Claim(Constants.Claims.AccessToken, accessToken));
// genereate the new ClaimsPrincipal
var claimsPrincipal = new ClaimsPrincipal(identity);
// store the original tokens in the AuthenticationProperties
var props = new AuthenticationProperties {
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1),
IsPersistent = false,
IssuedUtc = DateTimeOffset.UtcNow,
};
// sign in using the built-in Authentication Manager and ClaimsPrincipal
// this will create a cookie as defined in CookieAuthentication middleware
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, props).ConfigureAwait(false);
Uri uri = new Uri($"{Program.Settings.Shopify.RedirectUrl}?token={accessToken}");
return Redirect(uri.ToString());
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
So the above api will authorize my app in shopify and will return an access token. The accessToken is the one I want to save in the claims identity with Cookie authentication type(this is without authorizing user credentials). Still no errors at that point and after calling the HttpContext.SignInAsync function, I can still view using debugger the newly added claims.
As, you can see in the code, after assigning claims, I call to redirect the app to front-end link(Note: front-end and back-end has different url)
In my front-end app, I have a Nuxt middleware that I put a logic to check the token received from back-end since I only pass the token to the front-end app using query params. Here's my middleware code:
export default function ({ app, route, next, store, error, req }) {
if (process.browser) {
const shopifyAccessToken = store.get('cache/shopifyAccessToken', null)
if (!shopifyAccessToken && route.query.token) {
// if has token on query params but not yet in cache, store token and redirect
store.set('cache/shopifyAccessToken', route.query.token)
app.router.push({
path: '/',
query: {}
})
// verify access token on the route
app.$axios
.get(`/shopify/verifyaccess/${route.query.token}`)
.catch((err) => {
error(err)
})
} else if (!shopifyAccessToken && !route.query.token) {
// if does not have both, throw error
error({
statusCode: 401,
message: 'Unauthorized access to this app'
})
}
} else {
next()
}
}
In my middleware, when the route has query params equal to token= It calls another api to verify the accessToken saved in my claims identity:
[HttpGet("verifyaccess/{accessToken}")]
public async Task<IActionResult> VerifyAccess(string accessToken)
{
try
{
if (ModelState.IsValid)
{
var principal = HttpContext.User;
if (principal?.Claims == null)
return Unauthorized(_localizer["NotAuthenticated"]);
var accessTokenClaim = principal.FindFirstValue(Constants.Claims.AccessToken);
if (accessToken == accessTokenClaim)
{
return Ok();
}
else
{
return Unauthorized(_localizer["NotAuthenticated"]);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
Looking at the code above, it always fails me because the claims identity that I saved on the authorize endpoint was not there or in short the ClaimsIdentity is always empty.
Here's how I register the Cookie config:
private void ConfigureAuthCookie(IServiceCollection services)
{
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.RequireAuthenticatedSignIn = false;
})
.AddCookie(options => {
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
options.Cookie.Name = "shopifytoken";
});
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
}
and I also put a app.UseAuthentication() and app.UseAuthorization() on my Startup.Configure
Please let me know if this seems confusing so I can revised it. My main goal here is to be able to access that accessToken that I saved in the ClaimsIdentity so that I can verify the token. The reason why I did this because currently the shopify does not have an API for verifying access token. So when a user access my app link like this one http://example.com/?token=<any incorrect token> then they can already access my app.

Related

SignalR Azure Service with stand alone Identity Server 4 returns 401 on negotiaton

We have a ASP.Net Core application that authenticates against a standalone Identity Server 4. The ASP.Net Core app implements a few SignalR Hubs and is working fine when we use the self hosted SignalR Service. When we try to use the Azure SignalR Service, it always returns 401 in the negotiation requests. The response header also states that
"Bearer error="invalid_token", error_description="The signature key
was not found"
I thought the JWT-Configuration is correct because it works in the self hosted mode but it looks like, our ASP.Net Core application needs information about the signature key (certificate) that our identity server uses to sign the tokens. So I tried to use the same method like our identity server, to create the certificate and resolve it. Without luck :-(
This is what our JWT-Configuration looks like right now:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options => {
var appSettings = Configuration.Get<AppSettingsModel>();
options.Authority = appSettings.Authority;
options.RefreshOnIssuerKeyNotFound = true;
if (environment.IsDevelopment()) {
options.RequireHttpsMetadata = false;
}
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters {
ValidateAudience = false,
IssuerSigningKey = new X509SecurityKey(getSigningCredential()),
IssuerSigningKeyResolver = (string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters) =>
new List<X509SecurityKey> { new X509SecurityKey(getSigningCredential()) }
};
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
var accessToken = "";
var headerToken = context.Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", "");
if (!string.IsNullOrEmpty(headerToken) && headerToken.Length > 0) {
accessToken = headerToken;
}
var queryStringToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(queryStringToken) && queryStringToken.ToString().Length > 0) {
accessToken = queryStringToken;
}
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) {
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Update:
We also have a extended the signalR.DefaultHttpClient in our Angular Client and after playing around a bit, I noticed the application is working fine without it:
export class CustomSignalRHttpClientService extends signalR.DefaultHttpClient {
userSubscription: any;
token: string = "";
constructor(private authService: AuthorizeService) {
super(console); // the base class wants a signalR.ILogger
this.userSubscription = this.authService.accessToken$.subscribe(token => {
this.token = token
});
}
public async send(
request: signalR.HttpRequest
): Promise<signalR.HttpResponse> {
let authHeaders = {
Authorization: `Bearer ${this.token}`
};
request.headers = { ...request.headers, ...authHeaders };
try {
const response = await super.send(request);
return response;
} catch (er) {
if (er instanceof signalR.HttpError) {
const error = er as signalR.HttpError;
if (error.statusCode == 401) {
console.log('customSignalRHttpClient -> 401 -> TokenRefresh')
//token expired - trying a refresh via refresh token
this.token = await this.authService.getAccessToken().toPromise();
authHeaders = {
Authorization: `Bearer ${this.token}`
};
request.headers = { ...request.headers, ...authHeaders };
}
} else {
throw er;
}
}
//re try the request
return super.send(request);
}
}
The problem is, when the token expires while the application is not open (computer is in sleep mode e.g.), the negotiaton process is failing again.
I finally found and solved the problem. The difference of the authentication between "self hosted" and "Azure SignalR Service" is in the negotiation process.
Self Hosted:
SignalR-Javascript client authenticates against our own webserver with
the same token that our Javascript (Angular) app uses. It sends the
token with the negotiation request and all coming requests of the
signalR Http-Client.
Azure SignalR Service:
SignalR-Javascript client sends a negotiation request to our own
webserver and receives a new token for all coming requests against the
Azure SignalR Service.
So our problem was in the CustomSignalRHttpClientService. We changed the Authentication header to our own API-Token for all requests, including the requests against the Azure SignalR Service -> Bad Idea.
So we learned that the Azure SignalR Service is using it's own token. That also means the token can invalidate independently with our own token. So we have to handle 401 Statuscodes in a different way.
This is our new CustomSignalRHttpClientService:
export class CustomSignalRHttpClientService extends signalR.DefaultHttpClient {
userSubscription: any;
token: string = "";
constructor(private authService: AuthorizeService, #Inject(ENV) private env: IEnvironment, private router: Router,) {
super(console); // the base class wants a signalR.ILogger
this.userSubscription = this.authService.accessToken$.subscribe(token => {
this.token = token
});
}
public async send(
request: signalR.HttpRequest
): Promise<signalR.HttpResponse> {
if (!request.url.startsWith(this.env.apiUrl)) {
return super.send(request);
}
try {
const response = await super.send(request);
return response;
} catch (er) {
if (er instanceof signalR.HttpError) {
const error = er as signalR.HttpError;
if (error.statusCode == 401 && !this.router.url.toLowerCase().includes('onboarding')) {
this.router.navigate([ApplicationPaths.Login], {
queryParams: {
[QueryParameterNames.ReturnUrl]: this.router.url
}
});
}
} else {
throw er;
}
}
//re try the request
return super.send(request);
}
}
Our login-Route handles the token refresh (if required). But it could also happen, that our own api-token is still valid, but the Azure SignalR Service token is not. Therefore we handle some reconnection logic inside the service that creates the SignalR Connections like this:
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(async (page: NavigationEnd) => {
if (page.url.toLocaleLowerCase().includes(ApplicationPaths.Login)) {
await this.restartAllConnections();
}
});
hope this helps somebody

razor pages with firebase auth - where to put this token ? :)

i am working on web site with razor pages. part of the site should be accessed only by registred users. decided to go with firebase authentification (now with login and password ).
created everything necessary in firebase.
created backend code for user registration - works well.
created area which requires authorisation
services.AddRazorPages(options =>
{
options.Conventions.AuthorizeAreaFolder("User", "/");
})
added jwt middleware
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
added code to login page to call firebase to get token
function login()
{
firebase.auth().signInWithEmailAndPassword(email, password)
.then((userCredential) => {
// Signed in
var user = userCredential.user;
// ...
alert("signed");
})
.catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
alert(errorMessage);
});
}
got token from firebase.
if i'd call service next, i'd simply put token in "bearer" header.
tried to find how to add header to current browser for future requests and failed.
as i understand, i need this token to be added to auth header ? how ? :)
feeling dumb ;( tried to google, but most samples are for using this token later with api calls.
or i am going in the wrong direction?
tia
ish
well. it seems that it is not possible to add bearer from js, so i switched to cookies
in startup.cs use cookies
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["bearer"];
return Task.CompletedTask;
}
};
code to login with firebase, put token into the cookie and redirect
function login() {
firebase.auth().signInWithEmailAndPassword(email, password)
.then((userCredential) => {
// Signed in
var user = userCredential.user;
firebase.auth().currentUser.getIdToken(true).then(function (idToken)
{
document.cookie = "bearer" + "=" + idToken;
window.location.href = "/user/index";
}).catch(function (error) {
// Handle error
});
alert("signed");
})
.catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
alert(errorMessage);
});
}
or the same with firebaseUI
function login1()
{
ui.start('#firebaseui-auth-container', {
signInSuccessUrl: '/User/index',
signInOptions: [
{
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
requireDisplayName: false
}
],
callbacks:
{
signInSuccessWithAuthResult: function (authResult, redirectUrl)
{
var user = authResult.user;
firebase.auth().currentUser.getIdToken(true).then(function (idToken) {
document.cookie = "bearer" + "=" + idToken;
}).catch(function (error) {
// Handle error
});
return true;
}
}
});
}

HttpContext.SignInAsync not starting a new session for database users in identity server 4

I want to validate and use my database users in identity server 4.Here is my customized login code in Account Controller.
if (ModelState.IsValid)
{
// validate username/password against my user repository class, and get the user's info
var user = await _users.FindAsync(model.Username, model.Password);
if(user != null)
{
await _events.RaiseAsync(new UserLoginSuccessEvent(user.LoginId, user.SubjectId, user.FullName, clientId: context?.Client.ClientId));
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.FullName,
AdditionalClaims=user.Claims.ToList(),
AuthenticationTime=DateTime.UtcNow
};
await HttpContext.SignInAsync(isuser, props);
if (context != null)
{
if (context.IsNativeClient())
{
return this.LoadingPage("Redirect", model.ReturnUrl);
}
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.Client.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
I have created the Profile service class and configured the startup class like this
services.AddIdentityServer()
.AddInMemoryClients(InMemoryConfig.GetClients())
.AddProfileService<ProfileService>()
.AddInMemoryApiScopes(InMemoryConfig.GetApiScopes())
.AddInMemoryIdentityResources(InMemoryConfig.GetIdentityResources())
.AddDeveloperSigningCredential();
services.AddTransient<IProfileService, ProfileService>();
services.AddTransient<IUserRepository, UserRepository>();
But the user session never starts, takes me back to the login page and Profile service never gets called.
Where am I going wrong?
Sorry, guys my bad. This problem arises when SSL is disabled for my identity server 4 application. I enabled SSL for my application and everything started working fine.

User.Identity.IsAuthenticated returns false after login while using OpenId Connect with Auth0

I am trying to implement user authentication in an ASP.Net Core (v2.1) MVC application using OpenId Connect and Auth0. I have the required configurations stored in the AppSettings files and application runs well till the Auth0 login page comes. Post login it hits the Callback URL which basically invokes a method (method name is Callback) in my Account Controller. In the callback method I am trying to get the access token if the user is authenticated. However, the User.Identity.IsAuthenticated returns false. Here is my code in the Startup.cs file--
public void ConfigureServices(IServiceCollection services)
{
//Set Cookie Policy
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Add authentication services
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options => {
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("openid");
options.CallbackPath = new PathString("/oauth/callback");
options.ClaimsIssuer = "Auth0";
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
And here is my code in the Account Controller
public class AccountController : Controller
{
public async Task Login(string returnUrl = "/")
{
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = returnUrl });
}
[Authorize]
public async Task Logout()
{
await HttpContext.SignOutAsync("Auth0", new AuthenticationProperties
{
RedirectUri = Url.Action("Index", "Home")
});
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
public IActionResult AccessDenied()
{
return View();
}
[Authorize]
public IActionResult Claims()
{
return View();
}
[Route("/oauth/callback")]
public async Task<ActionResult> CallbackAsync()
{
if (User.Identity.IsAuthenticated)
{
string accessToken = await HttpContext.GetTokenAsync("access_token");
}
return RedirectToAction("Claims", "Account");
}
}
Please help. Any help will be appreciated.
Thanks,
Amit Anand
In fact i'm not sure why your custom CallbackAsync method fires during OIDC login . The callback url of OIDC middleware will handle token valiation ,token decode,exchange token and finally fill the user principle . You shouldn't handle the process and let OIDC middlware handle it , so change the route of the CallbackAsync method(or change the CallbackPath in OIDC middleware , but of course the url should match the url config in Auth0's portal ) , for example : [Route("/oauth/callbackAfterLogin")] .
After change that , the process will be : user will be redirect to Auth0 for login -->Auth0 validate the user's credential and redirect user back to url https://localhost:xxx/oauth/callback-->OIDC middlware handle token --> authentication success . If you want to redirect to CallbackAsync(route is /oauth/callbackAfterLogin) and get tokens there , you can directly pass the url in ChallengeAsync method when login :
await HttpContext.ChallengeAsync("Auth0",
new AuthenticationProperties() { RedirectUri = "/oauth/callbackAfterLogin"});

Refresh token and roles are missing (OpenIddict)

my tokens are missing refresh and role property. I am using OpenIddict. The code did work until today and it still works on home computer, but not on work.
I am pretty sure I did something wrong, but since I compare startup.cs, AuthorizationController.cs and they are the same (work and home), I need some help what could be the source of problem.
I need to get roles for user which logins, because my Angular2 application needs to know what a user can do on web page.
Request I sent:
Work response:
Home response:
Startup code (again same on home computer):
services.AddOpenIddict<int>()
.AddEntityFrameworkCoreStores<AppDbContext>()
.AddMvcBinders()
.EnableTokenEndpoint("/API/authorization/token")
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.UseJsonWebTokens()
.AddEphemeralSigningKey() //todo naj bi bil pravi certifikat, če odstranič to vrstico ne dela in vidiš error.
.SetAccessTokenLifetime(TimeSpan.FromMinutes(30))
.SetRefreshTokenLifetime(TimeSpan.FromDays(14))
.DisableHttpsRequirement();
Controller code (again: same on home computer):
public class AuthorizationController : BaseController
{
public AuthorizationController(AppDbContext context, OpenIddictApplicationManager<OpenIddictApplication<int>> applicationManager, SignInManager<AppUser> signInManager, UserManager<AppUser> userManager) : base(context, applicationManager, signInManager, userManager)
{
}
[Authorize, HttpGet("authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
Debug.Assert(request.IsAuthorizationRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
// Retrieve the application details from the database.
var application = await applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted);
if (application == null)
{
return View("Error", new ErrorViewModel
{
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
[HttpPost("token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType())
{
var user = await userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The email/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (userManager.SupportsUserTwoFactor && await userManager.GetTwoFactorEnabledAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (userManager.SupportsUserLockout && await userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await userManager.CheckPasswordAsync(user, request.Password))
{
if (userManager.SupportsUserLockout)
{
await userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (userManager.SupportsUserLockout)
{
await userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the refresh token.
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
OpenIdConnectServerDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token.
var user = await userManager.GetUserAsync(info.Principal);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The refresh token is no longer valid."
});
}
// Ensure the user is still allowed to sign in.
if (!await signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The user is no longer allowed to sign in."
});
}
// Create a new authentication ticket, but reuse the properties stored
// in the refresh token, including the scopes originally granted.
var ticket = await CreateTicketAsync(request, user, info.Properties);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(
OpenIdConnectRequest request, AppUser user,
AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await signInManager.CreateUserPrincipalAsync(user);
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties,
OpenIdConnectServerDefaults.AuthenticationScheme);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
}
ticket.SetResources("OpPISWeb"); //also in startup.cs
return ticket;
}
}
For decoding id_token I am using angular-jwt:
return this.http.post('api/authorization/token', this.encodeObjectToParams(data), options)
.map(res => res.json())
.map((tokens: AuthTokenModel) =>
{
console.log("loged in", tokens);
let now = new Date();
tokens.expiration_date = new Date(now.getTime() + tokens.expires_in * 1000).getTime().toString();
localStorage.setItem('id_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
const profile = this.jwtHelper.decodeToken(tokens.id_token) as ProfileModel;
const roles: string[] = typeof profile.role === "string" ? [profile.role] : profile.role;
const userProfile: Profile = new Profile(parseInt(profile.sub), roles);
localStorage.setItem('profile', JSON.stringify(userProfile));
this.refreshTokens(tokens.expires_in * 1000 * 0.8);
return profile;
});
The behavior you're seeing was caused by a bug introduced Friday. I fixed it a few minutes ago and new packages are being published at this moment.
Thanks for reporting it.