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

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.

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?

AWS Cognito Change Password .Net Core 3 MVC

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

Use JWT with OAuth Authentication in .Net-Core

I have a custom implementation of .AddOAuth() in .Net-Core. I've created a nuget package for Authentication using Coinbase (which is basically a clone of the add google implementation plus a few custom options specific to coinbase) full source. I've looked at a few other questions on this however they don't seem to implement OAuth (e.g I cannot pass scopes) I would like to login using OAuth But I want to return to my clients a JWT.
When I try to use JWT with AddCoinbase ( which is just a derrivative of AddOAuth)
services.AddAuthentication(JWT_BEARER_AUTH)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
//TODO: get key from secret section
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
})
.AddCoinbase(options => {
options.AccessAllAccounts = true;
options.SendLimitAmount = 1;
options.SendLimitCurrency = "USD";
options.SendLimitPeriod = SendLimitPeriod.day;
options.ClientId = Configuration["Coinbase:ClientId"];
options.ClientSecret = Configuration["Coinbase:ClientSecret"];
COINBASE_SCOPES.ForEach(scope => options.Scope.Add(scope));
options.SaveTokens = true;
options.ClaimActions.MapJsonKey("urn:coinbase:avatar", "avatar_url");
});
After I login to coinbase the external callback redirects me
[HttpGet("ExternalLoginCallback")]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
//TODO: Handle remote error failure
throw new Exception($"Error from external provider: {remoteError}");
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
//TODO: Handle null external login info
throw new Exception("Error: could not find user info");
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);1
var user = await (result.Succeeded ?
_userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey)
: this.CreateIdentityUser(info));
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider);
return Redirect(returnUrl);
}
After the redirect I never receive a JSON Web Token I always receive a Cookie. How can I leverage OAuth Authentication while serving JWT to my Clients?
OAuth is not a Json Web Token solution. OAuth 2.0 provides authorization and optionally identification (OIDC).
When you authorize via an OAuth 2.0 endpoint, you receive an Access Token and optionally an ID Token. The ID Token is a Signed JWT. The Access Token is an opaque object that is a Signed JWT for some vendor implementations but not all (Google is opaque).
After authorization you receive one or two tokens (access and ID). You can wrap them in your own JWT, sign it and then use the combined JWT any way that you want.

ASP.NET Core 2 - Angular & JWT Authentication

Problem: I seem unable to fetch the User or any user-related data (e.g. UserID) in any controller after the token has been recorded to browser local storage.
I've set a breakpoint and studied HttpContext member of ControllerBase instance (the client app makes request + the auth_token is kept in local storage at this stage).
You only can extract the referrer url from Headers but there's no info about tokens.
Even if you create a cookie for the token - the Request doesn't have it (0 cookies found at all).
Perhaps I misunderstand the concept of how authorization works.
Here's the bit I misunderstand most - how does ASP.NET Core fetch the token from the request made by client app - it must be kept in headers?
Also, could anyone share a working example of JWT Authentication where Angular & ASP.NET Core are separate solutions?
I've implemented login functionality and I store the access token in browser local storage.
this._authService.authenticate(this.loginModel)
.finally(finallyCallback)
.subscribe((result: AuthenticateOutput) => {
localStorage.setItem('auth_token', result.token);
});
Must the name of the token be in accordance with any conventions? (I wonder if auth_token is appropriate in this case.)
SessionController - the method which fetches current user info:
public async Task<GetCurrentLoginDetailsOutput> GetCurrentLoginDetails()
{
var output = new GetCurrentLoginDetailsOutput();
var user = await UserManager.GetUserAsync(HttpContext.User);
if (user != null)
{
output.User = Mapper.Map<UserDto>(user);
output.Tenant = Mapper.Map<TenantDto>(user.Tenant);
}
return output;
}
In my Authenticate method of AuthContoller I create Claim which holds UserID:
var user = await _userService.Authenticate(input.UserName, input.Password);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = _config.GetValidIssuer(),
Audience = _config.GetValidAudience(),
SigningCredentials = new SigningCredentials(_config.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256),
Subject = new ClaimsIdentity(new[]
{
new Claim("id", user.Id.ToString())
})
};
_userService.Authenticate method fetches the user and checks if the password is correct as follows:
var user = _context.Users.SingleOrDefault(x => x.UserName == username);
if (user == null) { return null; }
bool correctPassword = await UserManager.CheckPasswordAsync(user, password);
JWT config in Startup.cs
services
.AddAuthentication()
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = Configuration.GetSymmetricSecurityKey(),
ValidAudience = Configuration.GetValidAudience(),
ValidIssuer = Configuration.GetValidIssuer()
};
});
CORS is configured as follows:
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
Additional info:
The Angular app is a separate solution / project - not the "one solution" template available in VS2017.
I'm using ASP.NET Core v2.1
I'm using NSwag.AspNetCore package to auto-generate services for Angular project.
Here's the tutorial which I've been using to code my app.

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