Page loads before getting firebase access token and logs user out - asp.net-mvc

I am using firebase UI (Vanilla JavaScript) to sign in/ signup user in my ASP.NET Core application. The login and signup works perfectly the issue arises when the token gets expired which is after 1 hour. At that point firebase SDK refreshes the access token using refresh token. But sometimes access token is not fetched instantly and takes some time, during this time my application thinks that the user is unauthorized and logs them out and brings them back to login page. But later the access token finally comes and is set as a cookie and now at this point if I refresh the login page it redirects me to user profile meaning that the user was authenticated successfully.
This is my code for setting up the cookie every time the access token is changed
firebase.auth().onIdTokenChanged(async (user) => {
if (user) {
user.getIdToken().then(function (accessToken)
{
document.cookie = #Json.Serialize(CookieConfig.Value.CookieName) + "=" + accessToken.toString() + "; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT";
});
}
});
this is my code for verifying the jwt in cookie
services.AddAuthentication(x => {
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://securetoken.google.com/project-id";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://securetoken.google.com/project-id",
ValidateAudience = true,
ValidAudience = "project-id",
ValidateLifetime = true
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies[configuration.GetSection("CookieConfiguration")["cookieName"]];
return Task.CompletedTask;
}
};
});
PS: I am saving the access token as cookie every time it is changed and verifying it.

Related

Creating and validating JWT token in asp net core MVC application (which does not have client server approach)

I have seen many examples to generate and validate JWT token in WEP API.
WEP API will have client and server approach. Hence we will validate user and generate JWT token in server and send to client. Client will store the token in browser memory next time through httpclient token will attached in request header and send it again to server. Now server will validate those token before hitting controller and allow to access those resource.
But in MVC application we don't have client and server approach. it will send view pages as result to browser.
My question is in MVC controller I have validated the user and created JWT token,
Now how to store the token in client
How to attach the token in request header.
Where should I do the token validation logic in MVC.
Where should I do the refresh token logic in MVC.
Thanks in Advance
According to your question, here are several solutions.
Store token in cookie, this is the recommended practice. Example:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(config=>
{
config.Cookie.Name = "auth";
})
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
//...
};
});
Although the backend can serialize the token and send it to the view (using JavaScript to store the token in Localstorage), this is not safe. Cookie can avoid csrf attacks. It is suitable for single page application.
If you put it in LocalStorage or SessionStorage, you need to get the token first and put it in the header of the request (take ajax as an exmple). Otherwise, no other configuration is required.
beforeSend: function(request) {
request.setRequestHeader("Authorization", sessionStorage.getItem("Authorization"));
}
You need to add [Authorize] to some actions, it will trigger this authentication which you configed in service. Or you can add a action to parse the tocken to get the context.
When someone update own information, you can regenerate a tocken and then send it to view to update the LocalStorage or cookie. It will carry this token in the next request.
The view can send a request to authenticate.
public IActionResult Authenticate()
{
//...
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
Response.Cookies.Append("authname", tokenString);
return View("index");
}
In startup (ConfigureServices), you can config the getting method with cookie.
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(config=>
{
config.Cookie.Name = "authname";
})
.AddJwtBearer(o =>
{
o.Events = new JwtBearerEvents()
{
//get cookie value
OnMessageReceived = context =>
{
var a = "";
context.Request.Cookies.TryGetValue("authname", out a);
context.Token = a;
return Task.CompletedTask;
}
};
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
ValidIssuer = "http://localhost:5200",
ValidAudience = "api",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("this is a long key------------------------"))
//...
};
});

msal.js cannot get the refresh_token even though offline_access is available and I can see it in the return call

I use msal.js to get access to the Microsoft Graph Api and I have gotten it working for the post part.
As you can see on the images I get the refresh_token in the response payload but not in the actual output when I console log it, how do I do this ?
Then thing is I need this refresh_token as the the place they auth their microsoft account is not the same place as the data is actually shown.
Let me explain
We are a infoscreen company and we need this to show the calendar of the people who authed when they have inserted it on the presentation.
so the flow is as follows:
They install the app and login with their Microsoft 365 account to give us access to this data. (this is the part that returns the refresh token and access Token).
They go to the presentation and insert the app in the area they want to show it.
on the actual monitor that could stand anywhere in the world the calendar would now show up.
But after 1 hour the session would expire so we need to generate a new access_token and for this we need the refresh_token.
At the step of loginPopup I can see there is a refreshToken
but when I use the data it is gone, I also tried to request token silently
Also I updated to the newest version of msal-browser.min.js version 2.1 that should support it.
async function signInWithMicrosoft(){
$(".notification_box", document).hide();
$("#table_main", document).show();
const msalConfig = {
auth: {
clientId: '{CLIENTID}',
redirectUri: '{REDIRECTURI}',
validateAuthority: false
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
forceRefresh: false
},
};
const loginRequest = {
scopes: [
"offline_access",
"User.Read",
"Calendars.Read",
"Calendars.Read.shared"
],
prompt: 'select_account'
}
try {
const msalClient = new msal.PublicClientApplication(msalConfig);
const msalClientLoggedIn= await msalClient.loginPopup(loginRequest).then((tokenResponse) => { console.log(tokenResponse); });
msalClientAccounts = msalClient.getAllAccounts();
var msalInsertAccount = true;
var tableMainAsText = $("#table_main", document).text();
if(typeof msalClientLoggedIn.idTokenClaims !== 'undefined'){
if(tableMainAsText.indexOf(msalClientLoggedIn.idTokenClaims.preferred_username)>-1){
msalInsertAccount = false;
}
if(msalInsertAccount){
var tableRow = "<tr>"+
"<td>"+msalClientLoggedIn.idTokenClaims.name+" ("+msalClientLoggedIn.idTokenClaims.preferred_username+") <span style='display: none;'>"+msalClientAccounts[0].username+"</span><input type=\"hidden\" name=\"app_config[exchange_online][]\" class=\"exchange_online_authed_account\" value=\""+msalClientLoggedIn.idTokenClaims.preferred_username+","+msalClientLoggedIn.idTokenClaims.name+"\" /></td>"+
"<td class=\"last\">Fjern adgang</td>"+
"</tr>";
$("#table_body", document).append(tableRow);
$("#table_foot", document).hide();
}
}
}catch(error){
$(".notification_box", document).show();
}
}

Handle failed Silent Authentication in Open Id Connect

I have an ASP.NET site that uses Open Id Connect to authenticate with Identity Server.
When the authentication token is about to expire I have added a silent authentication(prompt=none) that will renew the token without showing any login dialog to the user.
This works fine as long as the user is still logged in to Identity Server.
If the user is no longer logged in, an "login_required" error is returned. I want to handle this error by just letting it fail silently and redirect the user back to the page where the authentication started.
When being returned with an error to the AuthenticationFailed notification, the RedirectUri dosen't seem to be available though. Are there any way to access the RedirectUri after getting the error?
My configuration looks something like this(abbreviated):
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions {
...
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
// if we failed to authenticate without prompt
if (context.ProtocolMessage.Error == "login_required")
{
context.HandleResponse();
//context.Response.Redirect("The url to the page where RedirectToIdentityProvider was triggered");
return Task.FromResult(0);
}
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.FromResult(0);
},
RedirectToIdentityProvider = async context =>
{
...
if (ShouldReAuthenticate())
{
context.ProtocolMessage.SetParameter("prompt", "none");
}
...
}
}
});
I managed to solve this by using the ISecureDataFormat.Unprotect() method to read the information in the state message.
It can probably could be done more elegantly, but something like this:
if (!string.IsNullOrEmpty(context.ProtocolMessage.State) ||
context.ProtocolMessage.State.StartsWith("OpenIdConnect.AuthenticationProperties="))
{
var authenticationPropertiesString = context.ProtocolMessage.State.Split('=')[1];
AuthenticationProperties authenticationProperties = context.Options.StateDataFormat.Unprotect(authenticationPropertiesString);
return authenticationProperties.RedirectUri;
}

How to handle posts with IdentityServer3 as authentication server

TL;DR
How do you POST data in an ASP.NET MVC project (form, jQuery, axios), using IdentityServer3 as the authentication server. Also, what flow to use, to make this work?
What I'm experiencing
I have a working IdentityServer3 instance. I also have an ASP.NET MVC project. Using hybrid flow, as I will have to pass the user's token to other services. The authentication itself works - when the pages are only using GET. Even if the authenticated user's tokens are expired, something in the background redirects the requests to the auth. server, and the user can continue it's work, without asking the user to log in again. (As far as I understand, the hybrid flow can use refresh tokens, so I assume that's how it can re-authenticate the user. Even if HttpContext.Current.User.Identity.IsAuthenticated=false)
For testing purposes, I set the AccessTokenLifetime, AuthorizationCodeLifetime and IdentityTokenLifetime values to 5 seconds in the auth. server. As far as I know, the refresh token's expire time measured in days, and I did not change the default value.
But when I try to use POST, things get "ugly".
Using form POST, with expired tokens, the request gets redirected to IdentityServer3. It does it's magic (the user gets authenticated) and redirects to my page - as a GET request... I see the response_mode=form_post in the URL, yet the posted payload is gone.
Using axios POST, the request gets redirected to IdentityServer3, but fails with at the pre-flight OPTIONS request.
Using the default jQuery POST, got same error. (Even though, the default jQuery POST uses application/x-www-form-urlencoded to solve the pre-flight issue.)
startup.cs
const string authType = "Cookies";
// resetting Microsoft's default mapper
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// ensure, that the MVC anti forgery key engine will use our "custom" user id
AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
app.UseCookieAuthentication(new Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions
{
AuthenticationType = authType
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
RedirectUri = adminUri,
PostLogoutRedirectUri = adminUri,
Authority = idServerIdentityEndpoint,
SignInAsAuthenticationType = authType,
ResponseType = "code id_token",
Scope = "openid profile roles email offline_access",
Notifications = new OpenIdConnectAuthenticationNotifications
{
#region Handle automatic redirect (on logout)
RedirectToIdentityProvider = async n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType ==
OpenIdConnectRequestType.LogoutRequest)
{
var token = n.OwinContext.Authentication.User.FindFirst(idTokenName);
if (token != null)
{
var idTokenHint =
token.Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
},
#endregion
AuthorizationCodeReceived = async n =>
{
System.Diagnostics.Debug.Print("AuthorizationCodeReceived " + n.ProtocolMessage.ToString());
// fetch the identity from authentication response
var identity = n.AuthenticationTicket.Identity;
// exchange the "code" token for access_token, id_token, refresh_token, using the client secret
var requestResponse = await OidcClient.CallTokenEndpointAsync(
new Uri(idServerTokenEndpoint),
new Uri(adminUri),
n.Code,
clientId,
clientSecret
);
// fetch tokens from the exchange response
identity.AddClaims(new []
{
new Claim("access_token", requestResponse.AccessToken),
new Claim("id_token", requestResponse.IdentityToken),
new Claim("refresh_token", requestResponse.RefreshToken)
});
// store the refresh_token in the session, as the user might be logged out, when the authorization attribute is executed
// see OrganicaAuthorize.cs
HttpContext.Current.Session["refresh_token"] = requestResponse.RefreshToken;
// get the userinfo from the openId endpoint
// this actually retreives all the claims, but using the normal access token
var userInfo = await EndpointAndTokenHelper.CallUserInfoEndpoint(idServerUserInfoEndpoint, requestResponse.AccessToken); // todo: userinfo
if (userInfo == null) throw new Exception("Could not retreive user information from identity server.");
#region Extract individual claims
// extract claims we are interested in
var nameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name)); // full name
var givenNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName)); // given name
var familyNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName)); // family name
var emailClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email)); // email
var subClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject)); // userid
#endregion
#region Extract roles
List<string> roles;
try
{
roles = userInfo.Value<JArray>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role).Select(r => r.ToString()).ToList();
}
catch (InvalidCastException) // if there is only 1 item
{
roles = new List<string> { userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role) };
}
#endregion
// attach the claims we just extracted
identity.AddClaims(new[] { nameClaim, givenNameClaim, familyNameClaim, subClaim, emailClaim });
// attach roles
identity.AddClaims(roles.Select(r => new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role, r.ToString())));
// update the return value of the SecurityTokenValidated method (this method...)
n.AuthenticationTicket = new AuthenticationTicket(
identity,
n.AuthenticationTicket.Properties);
},
AuthenticationFailed = async n =>
{
System.Diagnostics.Debug.Print("AuthenticationFailed " + n.Exception.ToString());
},
MessageReceived = async n =>
{
System.Diagnostics.Debug.Print("MessageReceived " + n.State.ToString());
},
SecurityTokenReceived = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenReceived " + n.State.ToString());
},
SecurityTokenValidated = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenValidated " + n.State.ToString());
}
}
});
Have you configured cookie authentication middleware in the MVC app? After the authentication with identity server, an authentication cookie should be set. When the authentication cookie is set and valid IdentityServer redirection will not occur until the cookie expires/deleted.
Update 1:
Ok, I misunderstood the quesion. It is logical to redirect to identity server when session times out. It won't work with post payload. You can try doing something like follows.
If the request is a normal post, redirect user again to the form
fill page.
If request is ajax post, return unauthorized result and based on
that response refresh the page from javascript.
Anyway I don't think you will be able to keep the posted data unless you are designing your own solution for that. (e.g keep data stored locally).
But you might be able to avoid this scenario altogether if you carefuly decide identity server's session timeout and your app's session timeout.
In OpenIdConnectAuthenticationOptions set UseTokenLifetime = false that will break connection between identity token's lifetime and cookie session lifetime.
In CookieAuthenticationOptions make sliding expiration
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(50),
Now you are incontrol of your apps session lifetime. Adjust it to match your needs and security conserns.

IdentityServer MVC Token Expiration

I am new to Identity server and a key concept is missing from my understanding.
I am using the code from the MVC tutorial.
If I decorate my Home controller with the attribute [Authorize] and visit my website I get redirect to the IdentityServer. I then log in using my username and password. I then use some custom code and authenticate. I get back an AccessToken and I can then access the Home controller.
My client settings is as follows:
new Client {
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = new List<Secret>{new Secret("secret".Sha256())},
RequireConsent = false,
AccessTokenLifetime = 1,
// where to redirect to after login
RedirectUris = new List<string>{"http://localhost:5002/signin-oidc"},
// where to redirect to after logout
PostLogoutRedirectUris = new List<string>{"http://localhost:5002"},
AllowedScopes = new List<string>
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
}
}
My access token is
{
"nbf": 1474697839,
"exp": 1474697840,
"iss": "http://localhost:5000",
"aud": "http://localhost:5000/resources",
"client_id": "mvc",
"scope": [
"openid",
"profile"
],
"sub": "26296",
"auth_time": 1474697838,
"idp": "local",
"amr": [
"pwd"
]
}
As I set my AccessTokenLifetime to 1 my token when sent to call an API etc will be invalided. I still however will be able to access the website.
What is the best way to get the MVC website to confirm that my token has not expired? This may be where the refresh tokens come in to play.
Note
The AccessTokenLifetime set to 1 is for testing only so I can test things quickly.
You will need to set the user authentication lifetime to match that of your access token. If using OpenIdConnect you can do that with the following code:
.AddOpenIdConnect(option =>
{
...
option.Events.OnTicketReceived = async context =>
{
// Set the expiry time to match the token
if (context?.Properties?.Items != null && context.Properties.Items.TryGetValue(".Token.expires_at", out var expiryDateTimeString))
{
if(DateTime.TryParse(expiryDateTimeString, out var expiryDateTime))
{
context.Properties.ExpiresUtc = expiryDateTime.ToUniversalTime();
}
}
};
});
I assume you are using cookie authentication? If so you may have to switch off Sliding Expiration. Sliding Expiration will automatically refresh the cookie any time it processes a request which was more than halfway through the expiration window. However the access token won't be refreshed as part of this. Therefore you should let the user authentication lifetime run until the end, at which point it will expire and a new access token will be automatically retrieved using the refresh token.
.AddCookie(options =>
{
// Do not re-issue a new cookie with a new expiration time.
// We need to let it expire to ensure we get a fresh JWT within the token lifetime.
options.SlidingExpiration = false;
})
Maybe like this?
var user = HttpContext.Current.User.Identity;
if (!user.IsAuthenticated)

Resources