I have an Asp.net 2.0 core web application which connects to an Identity server 4 application for authentication. There is also an API involved. The API consumes an access token as a bearer token.
My startup:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = idsEndPoint;
options.RequireHttpsMetadata = false;
options.ClientId = "testclient";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("testapi");
});
Controller:
In my controllers i can see my tokens and they are all populated and i can use the access token in my API calls.
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var refreshToken = await HttpContext.GetTokenAsync(IdentityConstants.HttpContextHeaders.RefreshToken);
var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
Question:
My problem occurs after one hour where the access token expires. It appears that it is not automatically being refreshed. I am wondering if this is a setting in my authentication that will cause it to refresh it. However I have been unable to find out how I am supposed to force it to refresh the access token after it has expired.
My current solution is to refresh it myself but I would have thought this would be built into the cookie middleware.
for automatic refresh token, add options.Scope.Add("offline_access"); to AddOpenIdConnect() options.
This approach uses OpenIddict, you need to implement the main configuration inside startup.cs. The next Link is an excellent example of this implementation. Hope be useful
https://github.com/openiddict/openiddict-samples/tree/dev/samples/RefreshFlow
if (request.IsPasswordGrantType())
{
if (!Email_Regex_Validation.Check_Valid_Email_Regex(request.Username))
{
return BadRequest(Resources.RegexEmail);
}
SpLoginUser stored = new SpLoginUser(_context);
string result = stored.Usp_Login_User(request.Username, request.Password);
if (!result.Contains("successfully"))
{
return Forbid(OpenIddictServerDefaults.AuthenticationScheme);
}
// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
OpenIddictServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
identity.AddClaim(Resources.issuer, Resources.secret,
OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, request.Username,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OfflineAccess);
// Ask OpenIddict to generate a new token and return an OAuth2 token response.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
Related
We're using AWS Cognito for user accounts and using their hosted login form. Today I realized that there is not a "Change password" feature anywhere I can find on the Cognito Hosted Web UI. Is this true? If it is I need to figure out how to build a change password form.
The app is built in .NET Core 3.1, and I have added the AWSSDK.CognitoIdentityProvider NuGet package.
I get an access token after a user logs into Cognito and is redirected back to my app via OpenIdConnect. In my Startup.cs I have this code inside .AddOpenIdConnect() which is within ConfigureServices() which I thought was giving me an access token for the user:
options.Events = new OpenIdConnectEvents()
{
OnTokenValidated = context =>
{
// Access Token
var accessToken = context.SecurityToken.RawData;
var option = new Microsoft.AspNetCore.Http.CookieOptions();
option.Expires = new DateTimeOffset(context.SecurityToken.ValidTo);
context.HttpContext.Response.Cookies.Append("CognitoAccessToken", accessToken, option);
return Task.CompletedTask;
},
}
Then in another request, I get that same token and pass it into ChangePasswordAsync:
var cognitoClient = new Amazon.CognitoIdentityProvider.AmazonCognitoIdentityProviderClient("admin-level-access-key-id", "admin-level-secret-access-key");
var testClientWorks = await cognitoClient.DescribeUserPoolAsync(new Amazon.CognitoIdentityProvider.Model.DescribeUserPoolRequest { UserPoolId = "...my pool id..." });
var token = HttpContext.Request.Cookies["CognitoAccessToken"].ToString();
var response = await cognitoClient.ChangePasswordAsync(new Amazon.CognitoIdentityProvider.Model.ChangePasswordRequest
{
AccessToken = token,
PreviousPassword = "oldpassword",
ProposedPassword = "Test1234$"
});
The call to DescribeUserPoolAsync works, so I know the credentials for the cognitoClient are valid. But the call to ChangePasswordAsync fails with the error 'Invalid Access Token'.
So, if the access token I get back when they log in is not good, where do I get a valid one?
Edit:
So it turns out that I have an ID token and not an access token. I think this is because of how I configured OpenIdConnect. Changing options.ResponseType to token like below results in an error: Exception: OpenIdConnectAuthenticationHandler: message.State is null or empty.
options.SignInScheme = "Cookies";
options.Authority = $"https://cognito-idp.{awsCognitoRegion}.amazonaws.com/{awsCognitoPoolId}";
options.RequireHttpsMetadata = true;
options.ClientId = awsCognitoClientId;
options.ClientSecret = awsCognitoSecret;
//options.ResponseType = "code"; //what I was using before
options.ResponseType = "token";
options.UsePkce = true;
//options.Scope.Add("profile");
//options.Scope.Add("offline_access"); //results in invalid scope error
options.Scope.Add("openid");
//options.Scope.Add("aws.cognito.signin.user.admin");
options.SaveTokens = true;
////Tell .Net Core identity where to find the "name"
options.TokenValidationParameters.NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
options.TokenValidationParameters.AuthenticationType = IdentityConstants.ApplicationScheme;
////options.TokenValidationParameters.NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Clear(); //fixes something in .NET Core
This is how the OAuth 2.0 section is configured in Cognito:
It turns out the tokens were being set behind the scenes somewhere. So you can request them after login via:
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
I have a scenario where multiple clients have access to the same ASP.NET Core API . I.e. the same scopes. They are, however, not permitted to access the same data. If I could identify which client is accessing the API then it could validate whether it had access rights or not. I assume though that this is a bad approach - though doable in my scenario - because it requires code changes when new clients are added. Is there a way to have clients with bearer tokens having claims that I can read in my controller action? How do I configure this is and how do I access the claims in the API?
I am not familiar with IdentityServer so can't answer to the part about adding claims.
However once the user has claims, you can access it in your api quite simply:
[HttpPost("foo/{bar}")]
public async Task<IActionResult> Foo(string bar)
{
var user = User.FindFirst(ClaimTypes.Name).Value;//use whatever kind of claim type you want.
return await DoSomeWork();
}
Hope this helps.
You can add the client id(aud claim in token) to principle in OnTokenValidated event of AddOpenIdConnect extension .
In your MVC client , modify the OIDC configuration :
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = ctx =>
{
var clientID = ctx.SecurityToken.Claims.FirstOrDefault(c => c.Type == "aud")?.Value;
var claims = new List<Claim>
{
new Claim("ClientID", clientID)
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
},
};
});
Then you could read the client id in controller like :
var clientID = User.Claims.FirstOrDefault(c => c.Type == "ClientID")?.Value;
With using System.Linq; for FirstOrDefault Linq operation .
I have an ASP.NET MVC site, IdentityServer4 host and a web API.
When I log in the MVC site, using external provider (Facebook), I'm logged in fine. From the MVC site I can also consume the web API correctly.
However, the next day, I'm still logged in into the MVC site, but when I then try to access the web API, I get a 'not authorized exception'.
So although I'm still logged in in the MVC site, I'm not authenticated anymore to call a web API from within the MVC site.
I'm wondering how to handle this situation, and how IdentityServer4 should be configured.
Why am I still logged in the MVC site a day later? How can this be configured?
Why can't I still call the web API, if I'm still logged in the MVC site?
Can I sync the expiration times? Or how should I handle this?
The MVC application is configured like:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = mgpIdSvrSettings.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = mgpIdSvrSettings.ClientId;
options.ClientSecret = mgpIdSvrSettings.ClientSecret; // Should match the secret at IdentityServer
options.ResponseType = "code id_token"; // Use hybrid flow
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("mgpApi");
options.Scope.Add("offline_access");
});
So it's using hybrid flow.
In IdentityServer the MVC client is configured like:
new Client
{
EnableLocalLogin = false,
ClientId = "mgpPortal",
ClientName = "MGP Portal Site",
AllowedGrantTypes = GrantTypes.Hybrid,
// where to redirect to after login
RedirectUris = mgpPortalSite.RedirectUris,
// where to redirect to after logout
PostLogoutRedirectUris = mgpPortalSite.PostLogoutRedirectUris,
// secret for authentication
ClientSecrets = mgpPortalSite.ClientSecrets.Select(cs => new Secret(cs.Sha256())).ToList(),
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"mgpApi"
},
AllowOfflineAccess = true,
RequireConsent = false,
},
And finally the web API:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = mgpIdSvrSettings.Authority;
options.RequireHttpsMetadata = false;
options.ApiName = mgpIdSvrSettings.ApiName;
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10);
});
There are two types of authentication, cookie and bearer.
Where the cookie keeps you logged in, the bearer token can't. Because the bearer token is set to expire at some point, without allowing you to change the lifetime.
The only way to access the resource (api) after the access token expires is to either let the user login again or request a new access token using a refresh token, without needing user interaction.
You've already configured it:
options.Scope.Add("offline_access");
On each login the request will at least contain a refresh token. Store it at a safe place and use it when needed. By default it is set to one time use only.
You can use something like this code to renew the token (as you are not actually refreshing it, but rather replacing it). You'll need to include the 'IdentityModel' NuGet package, as seen in the samples from IdentityServer.
private async Task<TokenResponse> RenewTokensAsync()
{
// Initialize the token endpoint:
var client = _httpClientFactory.CreateClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError) throw new Exception(disco.Error);
// Read the stored refresh token:
var rt = await HttpContext.GetTokenAsync("refresh_token");
var tokenClient = _httpClientFactory.CreateClient();
// Request a new access token:
var tokenResult = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "mvc",
ClientSecret = "secret",
RefreshToken = rt
});
if (!tokenResult.IsError)
{
var old_id_token = await HttpContext.GetTokenAsync("id_token");
var new_access_token = tokenResult.AccessToken;
var new_refresh_token = tokenResult.RefreshToken;
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
// Save the information in the cookie
var info = await HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", new_refresh_token);
info.Properties.UpdateTokenValue("access_token", new_access_token);
info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
await HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
return tokenResult;
}
return null;
}
By default the refresh token usage is configured as one time use. Please note that when storing the new refresh token fails and you should lose it, then the only way to request a new refresh token is to force the user to login again.
Also note that the refresh token can expire.
And taking it one step back, you'll need to use this when the access token expired or is about to expire:
var accessToken = await HttpContext.GetTokenAsync("access_token");
var tokenHandler = new JwtSecurityTokenHandler();
var jwtSecurityToken = tokenHandler.ReadJwtToken(accessToken);
// Depending on the lifetime of the access token.
// This is just an example. An access token may be valid
// for less than one minute.
if (jwtSecurityToken.ValidTo < DateTime.UtcNow.AddMinutes(5))
{
var responseToken = await RenewTokensAsync();
if (responseToken == null)
{
throw new Exception("Error");
}
accessToken = responseToken.AccessToken;
}
// Proceed, accessToken contains a valid token.
I'm using ASP.NET Core 2.1 and Auth0.
When I try to retrieve the acces_token to access my own API I use
string accessToken = await HttpContext.GetTokenAsync("access_token");
The strange thing is when I paste the token on https://jwt.io/ it shows that an audience has been added. The thing is that two audiences are not allowed and so the token is not valid. The audience that is added ends with /userinfo
Can someone please explain why there are two audiences in my acces token?
I use the following code in ConfigureServices
// Add authentication services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options =>
{
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
// Set response type to code
options.ResponseType = "code";
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
// Set the callback path, so Auth0 will call back to http://localhost:5000/signin-auth0
// Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
options.CallbackPath = new PathString("/signin-auth0");
// Configure the Claims Issuer to be Auth0
options.ClaimsIssuer = "Auth0";
// Saves tokens to the AuthenticationProperties
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;
},
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("audience", "MY_OWN_AUDIENCE_URL");
return Task.FromResult(0);
}
};
});
Can someone please explain why there are two audiences in my acces token?
The second audience is the userinfo endpoint. The userinfo endpoint is part of the OpenID Connect protocol; it exposes the end-user's profile information and is present because of the openid scope.
When Auth0 receives an authorization request, it checks the request's audience and scope parameters. If the audience is a custom API, and if the scope includes openid, then the access_token will include two audiences: one for your custom API, the other for the Auth0 userinfo endpoint.
Here is a supporting quote from https://auth0.com/docs/tokens/access-token
When the audience is set to a custom API and the scope parameter includes the openid value, then the generated Access Token will be a JWT valid for both retrieving the user's profile and for accessing the custom API. The aud claim of this JWT will include two values: YOUR_AUTH0_DOMAIN/userinfo and your custom API's unique identifier.
WORKING
I got it working with the next code placed in ConfigureServices in the Startup class. In the List from Configuration I placed the audience from Auth0 userinfo API and my own API.
// Multiple audiences
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudiences = Configuration.GetSection("Auth0:Audiences").Get<List<string>>(),
ValidateLifetime = true
};
I'm using Google authentication for my asp.net mvc application.
I added Google to my Startup.cs class:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = GoogleDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddGoogle(googleOptions =>
{
googleOptions.ClientId = _configuration["Authentication:Google:ClientId"];
googleOptions.ClientSecret = _configuration["Authentication:Google:ClientSecret"];
googleOptions.SaveTokens = true;
});
I can get access_token from controller using this:
var token = await HttpContext.GetTokenAsync("access_token").ConfigureAwait(false);
I need id_token to authenticate to my custom backend application like this.
I tried using this code but I get null.
var token = await HttpContext.GetTokenAsync("id_token").ConfigureAwait(false);
Is it possible to get id_token somehow?
By default, Google authentication implementation uses response_type=code. With this flow, you don't have id_token in response. To have it, response_type should be response_type=code id_token (from here).
You may override that BuildChallengeUrl method in derived YourGoogleHandler class, and change DI registration from .AddGoogle() to
.AddOAuth<GoogleOptions, YourGoogleHandler>
(GoogleDefaults.AuthenticationScheme, GoogleDefaults.DisplayName, googleOptions)
(code was taken from Microsoft.AspNetCore.Authentication.Google/GoogleExtensions.cs
The solution that worked for me..
services.AddAuthentication()
.AddOpenIdConnect(GoogleDefaults.AuthenticationScheme,
GoogleDefaults.DisplayName,
options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.Authority = "https://accounts.google.com";
options.ClientId = googleOAuthSettings.ClientId;
options.ClientSecret = googleOAuthSettings.ClientSecret;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.CallbackPath = "signin-google";
options.SaveTokens = true; //this has to be true to get the token value
options.Scope.Add("email");
});
OpenIdConnect provider for Google OAuth allow us to customise the ResponseType.
As per the link , OpenIdConnectHandler which appears to implement IAuthenticationSignOutHandler. So that's why regardless of what is in the discovery document (end session endpoint supported or not), if you use the AddOpenIdConnect(...), it will always register a handler which seemingly supports sign out. If you are using any IdentityServer4 quick start, then you can get rid of that error by checking with a condition in AccountService.cs --> BuildLoggedOutViewModelAsync method.
var providerSupportsSignOut = await
_httpContextAccessor.HttpContext.GetSchemeSupportsSignOutAsync(idp)
Here, we can add additional check like : idp != Google.