AWS Cognito Change Password .Net Core 3 MVC - c#

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);

Related

Unable to get refresh token in Blazor Application using B2C and AddMicrosoftIdentityWebApp

I have a Blazor application that currently uses id tokens only for authentication with an expiration of 24h. I would like to be able to use refresh tokens, but can't access them from the C# code.
However, I know that the app is correctly configured since I can get a refresh token using postman:
Here is what the startup looks like:
services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddMicrosoftIdentityWebApp(options =>
{
options.SignedOutRedirectUri = "/";
options.SignInScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters.SaveSigninToken = true;
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.IssuerValidator = ValidateSpecificIssuers;
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.LifetimeValidator = ValidateLifetime;
options.Events.OnRedirectToIdentityProvider = ctx =>
{
if (ctx.Properties.Items.ContainsKey("login_hint"))
ctx.ProtocolMessage.LoginHint = ctx.Properties.Items["login_hint"];
if (ctx.Properties.Items.ContainsKey("domain_hint"))
ctx.ProtocolMessage.DomainHint = ctx.Properties.Items["domain_hint"];
var request = ctx.HttpContext.Request;
ctx.ProtocolMessage.RedirectUri = $"{request.Scheme}://{request.Host}/signin-oidc";
return Task.FromResult(0);
};
options.Events.OnAuthenticationFailed = async ctx =>
{
if (ctx.Result?.Succeeded ?? false)
return;
if (ctx.Request.Path == "/auth/logout")
{
ctx.HandleResponse();
return;
}
var body = await ctx.Request.Body.ReadToStringAsync();
var form = await ctx.Request.ReadFormAsync();
ctx.Principal = null;
ctx.HttpContext.User = null;
Console.WriteLine($"Authentication failed:\r\n{ctx.Exception?.Message}\r\n{ctx.Exception?.StackTrace}");
ctx.Response.Redirect("/auth/logout");
ctx.HandleResponse();
return;
};
options.ResponseType = OpenIdConnectResponseType.CodeIdTokenToken;
options.TenantId = Configuration["AzureAdB2C:TenantId"];
options.Instance = Configuration["AzureAdB2C:Instance"];
options.ClientId = Configuration["AzureAdB2C:ClientId"];
options.Domain = Configuration["AzureAdB2C:Domain"];
options.SignedOutCallbackPath = Configuration["AzureAdB2C:SignedOutCallbackPath"];
options.SignUpSignInPolicyId = Configuration["AzureAdB2C:SignUpSignInPolicyId"];
options.ResetPasswordPolicyId = Configuration["AzureAdB2C:ResetPasswordPolicyId"];
options.EditProfilePolicyId = Configuration["AzureAdB2C:EditProfilePolicyId"];
options.ClientSecret = Configuration["AzureAdB2C:ClientSecret"];
options.SaveTokens = true;
options.Scope.Add("offline_access"); // in order to get the refresh token
}
)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["BlazorServer:Scope"] })
.AddDownstreamWebApi("BlazorServer", configuration.GetSection("BlazorServer"))
.AddSessionTokenCaches();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, (OpenIdConnectOptions options) =>
{
options.Scope.Add("offline_access");
options.Events.OnTokenValidated += ctx =>
{
// ctx.TokenEndpointResponse.RefreshToken is null!
var token = ctx.SecurityToken as JwtSecurityToken;
ctx.Principal.AddIdentity(new ClaimsIdentity(new Claim[]
{
new Claim("access_token", token.RawData)
}));
ctx.Principal.AddIdentity(new ClaimsIdentity(
token.Claims,
"jwt",
ctx.Options.TokenValidationParameters.NameClaimType,
ctx.Options.TokenValidationParameters.RoleClaimType
));
return Task.CompletedTask;
};
options.Events.OnRemoteFailure += async (RemoteFailureContext context) =>
{
var response = await context.Response.Body.ReadToStringAsync();
var responseQuery = response.Contains("consent_required") ? "&response=consent" : "";
context.Response.Redirect($"/Error?error={context.Failure}{responseQuery}");
context.HandleResponse();
};
});
My ultimate goal would be to have another token claim which woud be the refresh token (I already have access token).
I've read on another post that this token could be automatically handled by Microsoft MSAL: Get refresh token with Azure AD V2.0 (MSAL) and Asp .Net Core 2.0
However, it does not appear to be the case on my end, since the user gets automatically logged out upon token expiration. Here is the token configuration I have in B2C
<Metadata>
<Item Key="client_id">{service:te}</Item>
<Item Key="issuer_refresh_token_user_identity_claim_type">objectId</Item>
<Item Key="SendTokenResponseBodyWithJsonNumbers">true</Item>
<Item Key="token_lifetime_secs">500</Item>
<Item Key="id_token_lifetime_secs">300</Item>
<Item Key="refresh_token_lifetime_secs">86400</Item>
<Item Key="rolling_refresh_token_lifetime_secs">86400</Item>
</Metadata>
I've also seend that I could call GetTokenAsync(), but in order to call that I need a cache of users. Is that the only way of doing it?
Thanks a lot for stopping by :)
We can use the below workaround to get refresh token for our .net application .
Below are workaround you can follow:
-Try to add the following in your appsettings.json file
"OpenIdConnect": {
"ResponseType": "code id_token token",
"Scope": [ "offline_access", "https://xxx.onmicrosoft.com/xxxx-xxc-xxx-xxxx-xxxxxx/Management" ],
"SaveTokens": "true" // Save access token and refresh token
`}
Following in your startup.cs
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, options =>
{
Configuration.Bind("OpenIdConnect", options);
}
For complete information please refer the below links:-
MS Q&A:- Azure AD B2C: How to get the refresh token in a MVC app?
SO THREAD:- Azure B2C - Issuer (Azure AD) access token in Blazor

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?

AspNet.Security.Oauth.Spotify HttpContext.GetTokenAsync("spotify", "access_token") Returning a null token after second login+

When using the ASP.Net core 3.0 angular SPA individual account template, and the AspNet.Security.OAuth.Spotify nuget package. When A user logs in, I want to be able to get their spotify access token so I can preform actions on the user's behalf. However, when I call await HttpContext.GetTokenAsync("spotify", "access_token"); the results returns null.
I've debugged a bit and saw the spotify tokens on second+ login in the options.Events.OnCreatingTicket event, but I guess the token is just not passed around past that? I'm not really sure anymore.
Startup.cs
ConfigureServices
services.AddAuthentication()
.AddIdentityServerJwt()
.AddSpotify("spotify", options =>
{
options.ClientId = Configuration["SpotifySettings:ClientId"];
options.ClientSecret = Configuration["SpotifySettings:ClientSecret"];
options.CallbackPath = "/callback";
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SaveTokens = true;
String[] items = {
"playlist-read-private", "playlist-modify-public", "playlist-modify-private", "playlist-read-collaborative", "user-library-modify", "user-library-read", "user-read-email"
};
foreach (var item in items)
{
options.Scope.Add(item);
}
options.Events.OnRemoteFailure = (context) =>
{
// Handle failed login attempts here
return Task.CompletedTask;
};
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);
return Task.CompletedTask;
};
});
Code to try to retrieve token
if (!User.Identity.IsAuthenticated)
{
return StatusCode(403);
}
var client = httpClientFactory.CreateClient("spotify");
String spotifyToken = await HttpContext.GetTokenAsync("spotify", "access_token");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", spotifyToken);
var result = await client.GetAsync("v1/me/playlists");
I expected to get a spotify access_token that I can use to call the spotify api but spotifyToken just returns null.
On your callback handling method, in case of IdentityServer, on the ExternalController you read out the external identity of the temporary cookie - the result (in case of a success) holds both the Access & Refresh Token as Properties:
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result.Succeeded != true)
throw new Exception("External authentication error");
var tokens = result.Properties.GetTokens();
if (tokens?.Any() == true)
{
_logger.LogDebug("External authentication success resulted in provided access tokens: \r\n {0}.", string.Join(",", tokens.Select(d => $"{d.Name}:{d.Value}")));
}

Still logged in MVC site, but can't call web API

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.

How to refresh access token

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);
}

Categories