Replacing Cookie by Token based authentication in ASP.NET OWIN OpenIdConnect code authorization flow - asp.net

We have a web application written in ASP.NET that uses MVC for serving our Single Page Applications and Web API for ajax calls.
The authentication uses Microsoft.Owin and OpenIdConnect with Azure AD for Authority. The OAUTH flow is server side code authorization.
Then in Startup.Auth.cs we have
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
var cookieAuthenticationOptions = new CookieAuthenticationOptions()
{
CookieName = CookieName,
ExpireTimeSpan = TimeSpan.FromDays(30),
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
SlidingExpiration = true,
};
app.UseCookieAuthentication(cookieAuthenticationOptions);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthorizationCodeReceived = (context) =>
{
/*exchange authorization code for a token
stored on database to access API registered on AzureAD (using ADAL.NET) */
},
RedirectToIdentityProvider = (RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context) =>
{
/* Set the redirects URI here*/
},
});
}
When clicking on signin we navigate to an url whose routes map to the methods of the following MVC controller
public class AccountController : Controller
{
public void SignIn(string signalrRef)
{
var authenticationProperties = /* Proper auth properties, redirect etc.*/
HttpContext.GetOwinContext()
.Authentication.Challenge(authenticationProperties, OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
public void SignOut(string signalrRef)
{
var authenticationProperties = /* Proper auth properties, redirect etc.*/
HttpContext.GetOwinContext().Authentication.SignOut(authenticationProperties,
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
Then the end-user connected to our application is authenticated between our client apps and the ASP.net server using an ASP.NET cookie. We would like to use Token Based approach instead. If you are interested this is the reason.
I tried to replace
the Nuget package Microsoft.Owin.Security.Cookies by Microsoft.Owin.Security.OAuth and in Startup.cs
replacing
app.UseCookieAuthentication(cookieAuthenticationOptions); by app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
and in my AccountController we changed the challenge from HttpContext.GetOwinContext().Authentication.SignOut(authenticationProperties,
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType); to HttpContext.GetOwinContext().Authentication.SignOut(authenticationProperties,
OpenIdConnectAuthenticationDefaults.AuthenticationType, OAuthDefaults.AuthenticationType);
The problem is that with Cookie the set-cookie was automatically sent in web request respond when the flow completes while redirecting to the url we specified.
Where can I find the Bearer generated by OWIN with UseOAuthBearerAuthentication (if there is any) **, **Where and When should I send it back to my client SPAs
Note: an open source sample of what we are trying to do can be found in this github repository.

I think there are two approaches for you to consider.
Use javascript libraries to perform sign-in & token acquisition within your single page app. Then your backend is purely an web API, and can just use OAuth bearer middleware to authenticate requests. The backend doesn't know anything about signing the user in. We have a good sample that takes this approach here. If your backend needs to make API calls as well, you could consider the OnBehalfOf flow as well. I usually recommend this approach.
Use the OpenIDConnect middleware in your server to perform user sign-in and token acquisition. You might even be able to omit the usage of the CookieAuthenticationMiddleware entirely (although I'm not 100% sure). You can capture the token in the AuthorizationCodeReceived notification as you mention, and you could redirect back to your SPA with the token in the fragment of the URL. You could also have some route which delivers the tokens (which are cached on your server) down to your javascript. In either case, you'll need to ensure that an outside caller can't get access to your tokens.
The thing to keep in mind will be how you refresh tokens when they expire. If you use #1, most of it will be handled for you by libraries. If you use #2, you'll have to manage it more yourself.

Related

Use two Owin identities in the same asp.net web application

Is it possible to use OWIN with two different authentications in the same time, e.g. Microsoft and Google?
In my ASP.NET web application, users authenticate initially with Azure OpenIdConnect to use the application.
At some point, user needs to authenticate with Google to perform few queries with Google (without overriding the Microsoft identity that will continue to be used).
I noticed that whenever I use Context.GetOwinContext().Authentication.Challenge(properties, "Google"), the authentication succeeded and I can call Google's API, but the Microsoft related claims, tokens and the whole identity are lost and replaced with the Google one, and I cannot anymore call Microsoft API unless I ask users to login again.
Is there any way to hold both identities so I can use them based on the need?
Since there was no answers and I could figure out the solution, Thanks to OWIN team support on GitHub, below is the solution:
Objective: Authenticate with more than one provider, and maintain both claims, so application can call both providers API's at any time.
In my case, Users must authenticate first with Azure Active Directory (OpenIdConnect) to be allowed to enter my application, plus to call Microsoft Graph API. Users also need to authenticate with Google, to make calls to Google API.
For OpenIdConnect, I'm using the default creation by visual studio
without any changes, and this is not the topic here.
How to add the second provider?
Tell OWIN that you are using the Google authentication during startup.
When user is trying to call Google API, check if it has Google related claims. if yes, use the access token and simply call the Google API, if Not, it means this is the first call to Google, so ask the application to authenticate with Google.
Once Authenticated with Google, save the claims (without overwriting OpenIdConnect's claims), so it can be used next time when calling Google API.
Now, let's see the details:
Below the main authentication (OpenIDConnect), Tell OWIN that you are using Google (by the way, this applies to any other provider). The most important part, is to tell OWIN to use different cookies for Google to save Google's claims in separate cookies. If you miss this step, the Google claims will overwrite the OpenIdConnect Claims, and you will not be able to call Microsoft Graph anymore.
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = AuthenticationConfig.ClientId,
Authority = AuthenticationConfig.Authority,
PostLogoutRedirectUri = AuthenticationConfig.PostLogoutRedirectUri,
RedirectUri = AuthenticationConfig.RedirectUri,
Scope = $"{AuthenticationConfig.BasicSignInScopes} User.Read",
SaveTokens=true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync,
SecurityTokenValidated = OnSecurityTokenValidatedAsync,
},
CookieManager = new Utils.SameSiteCookieManager(
new SystemWebCookieManager())
}
);
// Define New Cookies for Google
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Google",
AuthenticationMode = AuthenticationMode.Passive,
CookieName = CookieAuthenticationDefaults.CookiePrefix + "External.DocuSign",
});
// Tell OWIN to use Google with the special Cookies type
app.UseGoogleAuthentication(new Microsoft.Owin.Security.Google.GoogleOAuth2AuthenticationOptions()
{
ClientId = "xxxxxxxxxxxxxxxxxxxxxxx",
ClientSecret = "xxxxxxxxxxxxxxxxxx",
SignInAsAuthenticationType = "Google",
Provider = new Microsoft.Owin.Security.Google.GoogleOAuth2AuthenticationProvider() { OnAuthenticated = OnGoogleAuthenticated }
});
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
Before calling Google API, Check if Google Claims already exist. If yes, extract the access token and call Google API. If not, it means this is the first time you're trying to call Google, so authenticate first, save the claims, and then call the API.
var result = await Request.GetOwinContext().Authentication.AuthenticateAsync("Google");
if (result == null) // No Claims found for Google
{
// Redirect to Google for authentication
var properties = new AuthenticationProperties() { RedirectUri = "/" };
Context.GetOwinContext().Authentication.Challenge(properties, "Google");
}
else
{
// Get the Access Token from the google Claims
var accessToken = result.Identity.Claims.FirstOrDefault(a => a.Type == "google_access_token").Value;
// Now CALL Google API
}
Save the Google Claims after authenticating with Google. This is again in StartupAuth.cs in continuation to app.UseGoogeAuthentication where we override the event of getting google response, and we save the token to claims.
private static Task OnGoogleAuthenticated(Microsoft.Owin.Security.Google.GoogleOAuth2AuthenticatedContext context)
{
// Save the access token to Google Claims, to be used in Google API calls
context.Identity.AddClaim(new Claim("google_access_token", context.AccessToken));
if (context.RefreshToken != null)
{
context.Identity.AddClaim(new Claim("google_refresh_token", context.RefreshToken));
}
var expiresInSec = (long)(context.ExpiresIn.Value.TotalSeconds);
context.Identity.AddClaim(new Claim("google_expires_in", expiresInSec.ToString()));
return Task.FromResult(0);
}

Aps .net IdentityServer4 authorize

I'm using IdentityServer4 with asp .net identity as authentication point. My APIs/WebApps call identity server to get access token.
Now, how to authorize uses before some action or inside action in my api/app controller?
I can add roles to access token and then in controller (in web api/web app) use AuthorizeAttribute and check if user IsInRole.
But it means that if I will change user roles, he will see it after logout-login (because roles are part of access token) or token has to expire.
I would like to ask identity server about user role(s) each time I need to authorize him to some action (especially to action like modify/delete some data).
Question how?
Or What I have to looking for?
So there's a few possible solutions here:
Make a call to the OIDC UserInfo Endpoint to obtain updated user claims on every request
Lower the cookie lifetime to refresh user info automatically more often
Implement a custom endpoint on IdentityServer for it to post profile change information to a list of subscribed clients (such as your webapp).
Have IdentityServer force single sign out when user profile data is changed
In terms of difficulty to implement, lowering cookie lifetime is the easiest (just change cookie expiration), but it doesn't guarantee up-to-date claims, and it is visible to the user (frequent redirects to IdentityServer, although no login is required if the access token lifetime is still valid)
Having the webapp call the UserInfo Endpoint on each request is the next easiest (see sample below) but has the worst performance implications. Every request will produce a round trip to IdentityServer.
The endpoint / subscriber model would have the lowest performance overhead. UserInfo requests to IdentityServer would ONLY occur when user profile information has actually changed. This would be a bit more complicated to implement:
On your IdentityServer project, you would need to modify changes to profile data, and post an http message to your webapp. The message could simply contain the user ID of the modified user. This message would need to be authenticated somehow to prevent malicious users from voiding legitimate user sessions. You could include a ClientCredentials bearer token for this.
Your webapp would need to receive and authenticate the message. It would need to store the changed user's ID somewhere accessible to the OnValidatePrincipal delegate (through a service in the DI container most likely)
The Cookie OnValidatePrincipal delegate would then inject this local service to check if user information has changed before validating the principal
Code Samples
Get updated UserInfo from endpoint on each call
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "NameOfYourCookieAuthSchemeHere",
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = async context =>
{
// Get updated UserInfo from IdentityServer
var accessToken = context.Principal.Claims.FirstOrDefault(c => c.Type == "access_token").Value;
var userInfoClient = new UserInfoClient("https://{IdentityServerUrlGoesHere}");
var userInfoResponse = await userInfoClient.GetAsync(accessToken);
// Invalidate Principal if Error Response
if (userInfoResponse.IsError)
{
context.RejectPrincipal();
await context.HttpContext.Authentication.SignOutAsync("NameOfYourCookieAuthSchemeHere");
}
else
{
// Check if claims changed
var claimsChanged = userInfoResponse.Claims.Except(context.Principal.Claims).Any();
if (claimsChanged)
{
// Update claims and replace principal
var newIdentity = context.Principal.Identity as ClaimsIdentity;
newIdentity.AddClaims(userInfoResponse.Claims);
var updatedPrincipal = new ClaimsPrincipal();
context.ReplacePrincipal(updatedPrincipal);
context.ShouldRenew = true;
}
}
}
}
});
Update On Subscribed Change Message from IdentityServer. This example supposes you've created a service (ex IUserChangedService) which stores userIds received at the endpoint from IdentityServer. I don't have samples of the webapp's receiving endpoint or a service.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "NameOfYourCookieAuthSchemeHere",
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = async context =>
{
// Get User ID
var userId = context.Principal.Claims.FirstOrDefault(c => c.Type == "UserIdClaimTypeHere");
var userChangedService = context.HttpContext.RequestServices.GetRequiredService<IUserChangedService>();
var userChanged = await userChangedService.HasUserChanged(userId);
if (userChanged)
{
// Make call to UserInfoEndpoint and update ClaimsPrincipal here. See example above for details
}
}
}
});
The asp.net core docs have an example of this as well, except working with a local database. The approach of wiring to the OnValidatePrincipal method is the same:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie#reacting-to-back-end-changes
Hope this helps!

How am I getting a windows identity in this code?

Related to this problem: Owin Stage Markers
I'm using owin and identity framework to init an IIS hosted web app with authentication ...
public static void Configure(IAppBuilder app, IKernel kernel)
{
// ensure that owin creates the required UserManager & sign in manager per owin instance
app.CreatePerOwinContext<ApplicationUserManager>((options, owinContext) => ApplicationUserManager.Create(options, owinContext, kernel));
app.CreatePerOwinContext<ApplicationSignInManager>((options, owinContext) => ApplicationSignInManager.Create(options, owinContext, kernel));
GlobalFilters.Filters.Add(new AuthorizeAttribute());
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
app.Use((context, next) =>
{
// figure out if the user is in fact authenticated if not use "Guest" as the username here
var userName = context.Request?.User?.Identity?.Name ?? "Guest";
//THE QUESTION:
// Why at this point is context.Request.User a windows user with a username of ""
return next.Invoke();
}).UseStageMarker(PipelineStage.PostAuthenticate);
}
I'm not using windows auth anywhere, only bearer auth, and on the server windows auth is disabled within IIS, so how am I getting this "empty" identity and how can I fix this to get my token based identity from the authorization info in the current request?
Hmm,
It seems that Identityframework falls back to this state when it's authenticated but couldn't find a match.
I had a basic auth string in the header where it was looking for a bearer token which it couldn't validate.
I'm pretty sure this is odd behaviour though, some sort of auth failure / security exception might be a better solution here.

Too many cookies OpenIdConnect.nonce cause error page "Bad Request - Request Too Long"

I'm using OWIN / OAuth with OpenId Connect authentication (Microsoft.Owin.Security.OpenIdConnect) in a C# ASP MVC web app. The SSO login with Microsoft account basically works, but from time to time I'm getting an error page on the browser that says Bad Request - Request Too Long.
I found out that this error is caused by too many cookies. Deleting cookies helps for some time, but after a while the problem comes back.
The cookies that cause the problem are set from OpenId framework, so there are dozens of cookies with names like OpenIdConnect.nonce.9oEtF53WxOi2uAw........
This is not SPA application, but some parts are refreshed periodically with ajax calls.
It turned out that the root cause was the Ajax call.
The problematic flow was
1) OAuth cookie got expired after some time
2) Expiration normally causes redirection the page to login.microsoft.com to refresh the cookie. In this step OAuth framework adds new nonce cookie to the response (every time)!
3) But Ajax doesn't handle redirections outside of the domain (cross-domain to login.microsoft.com). But the cookie was already appended to the page.
4) Next periodical Ajax call repeated the flow causing rapid increase of 'nonce' cookies.
Solution
I had to extend the "OWIN OpenId" framework setup code to handle Ajax calls differently - to prevent redirection and stop sending cookies.
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = ctx =>
{
bool isAjaxRequest = (ctx.Request.Headers != null && ctx.Request.Headers["X-Requested-With"] == "XMLHttpRequest");
if (isAjaxRequest)
{
ctx.Response.Headers.Remove("Set-Cookie");
ctx.State = NotificationResultState.HandledResponse;
}
return Task.FromResult(0);
}
}
});
}
The Ajax caller had to be adjusted too to detect 401 code and perform full page refresh (which caused a quick redirect to Microsoft authority).
For me the solution was to enforce the creation of an ASP.NET session.
Steps to reproduce:
Delete ASP.NET_SessionId cookie and idsrv cookie on a protected page
of your webapp
Reload page
Redirect to OIDC authentication store and authenticate
Redirect back to webapp => validation of the authentication fails,
because no asp.net session is available
Endless redirects until 'Request too long...' error happens
Solution:
Enforce session creation by adding
protected void Session_Start(object sender, EventArgs e)
{
}
to global.asax.cs.
OWIN and MVC may be deleting each other's cookies as described by the AspNetKatana github.
As a workaround that page suggests to explicitly use SystemWebCookieManager or SystemWebChunkingCookieManager (Microsoft.Owin.Host.SystemWeb 3.1.0).
There is also the Kentor Owin Cookie Saver as a workaround.
However, I would first try upgrading to Owin 4.1.0 (released november 2019), as this seems to fix it!
In my case the problem was the order in which I was configuring the application inside Startup.cs.
Reminder to self - always configure authentication first!
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = _clientId,
ClientSecret = _clientSecret,
Authority = _authority,
RedirectUri = _redirectUri
});
// configure the rest of the application...
}

Using Facebook access tokens obtained in mobile app to access ASP.Net WebAPI2 controller actions

Setup
Client: mobile app built on Cordova
Backend: ASP.net WebAPI2 (based on the standard template) configured with facebook login provider.
Problem
Having authenticated the user in the mobile app and received a Facebook access token, I pass this in subsequent requests as an HTTP header ("Authorization: Bearer "). This returns status 401 Unauthorized.
Questions
What am i missing here? How can i access the WebAPI controller actions based on the Facebook access token obtained on the mobile device?
On a high level, what i'm trying to achieve is this:
User opens mobile app and authenticates with Facebook
If user is not registered as a local user, he must choose a username to complete the registration
User is registered and can access API
I was facing the same problem and I found a really good solution here: http://codetrixstudio.com/mvc-web-api-facebook-sdk/
The WebApi web site can't understand the access token provided by Facebook. I guess it's because it hasn't been issued by itself (LOCAL AUTHORITY) but by an external provider. The approach explained in the link above is based on validating the token given by Facebook using it's API and recreating the access token.
So, you'll need some additional steps to achieve your goal.
The external providers have API so you can get information. For example, the https://graph.facebook.com/me?access_token={0} can be used to check if the token is valid. On the server side, you'll need to make a https web request to this URL passing the token (and the secret app as a proof, if the app is configured to ask it in Facebook).
Given the token is ok, you'll create an identity (ClaimsIdentity) using the information you've got at the API (id and username, for example). This identity will be needed to make an instance of the AuthenticationTicket class so you'll be able to issue a new access token for your Cordova app. Use this new bearer access token in the Authorization header of your https calls and your WebApi will recognized it as valid calls.
Ps. The good thing here is that you can set the token's expiration.
Since API's are stateless, there are multiple ways to secure it. In this case the mobile app has authenticated the user, but the API has not.
You can register the user's email and facebook ID into the database using a anonymous route. this can serve as both the login and register technically. (you could secure it with a clientid via OAuth if you don't want it fully open) along with thier current token. You verify the user against the facebook API on the server before registering of course just in case.
Create a custom route handler to secure the account controller or any other routes. The custom route handler would check the database for the current FB token and fb ID combo as well as token expire time, since you don't want to keep authenticating if it's expired.
Facebook has two types of tokens.
A Short lived token, initially created when you login and a long term token that lasts up to 60 days vs 1-2 hours.
for the api side, i suggest sticking to the short lived token and re-authenticate once expired but thats up to you.
You should grab a facebook SDK of your choice to verify and pull account info.
Example Secured Route:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "routename",
routeTemplate: "account",
constraints: null,
handler: new CustomFBHandler()
{
InnerHandler = new HttpControllerDispatcher(config)
},
defaults: new {controller = "default"}
);
}
Custom Handler: (note that you should pass any needed dependancies in the construtor)
public class CustomHandler : DelegatingHandler
{
public CustomHandler()
{
}
protected async Task<bool> IsAuthenticated(HttpRequestMessage requestMessage)
{
//Authenticate FB User Info HERE Against the Registered/logged in user....
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
bool isAuthenticated = false;
try
{
isAuthenticated = await IsAuthenticated(request);
}
catch (Exception e)
{
var response = request
.CreateResponse(HttpStatusCode.InternalServerError, new { status = new { code = 333, error = true, message = e.Message } }, new JsonMediaTypeFormatter());
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(Configuration.AuthenticationScheme));
return response;
}
if (!isAuthenticated)
{
var response = request
.CreateResponse(HttpStatusCode.Unauthorized,new {status=new{code=1,error=true,message="Authorization Failed"}},new JsonMediaTypeFormatter());
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(Configuration.AuthenticationScheme));
return response;
}
return await base.SendAsync(request, cancellationToken);
}
}
You should send the FB Token and Facebook user id in the Headers. Once authenticated you can use the token/id to pull the user info you need from the database.

Resources