CookieAuthenticationOptions.LoginPath value not used when also using app.UseOpenIdConnectAuthentication - asp.net-mvc

I am using OWIN middleware for cookie authentication and openIdConnect. Before I added openIdConnect authentication to my startup auth code the cookie authentication option, LoginPath was used as the destination for redirecting unauthenticated users. This worked really well and is the functionality I would like to keep.
However, when I added app.UseOpenIdConnectAuthentication to my project, it started automatically redirecting unauthenticated users to my OpenIdConnect Authority (https://login.windows.net/).
Is there a way I can disable OpenIdConnectAuthentication setting the redirect path for unauthenticated users and rely on the LoginPath set for cookie authentication? My current work around is to manually set the redirect path in my authorize attribute, but I would like to let OWIN middleware handle this if possible.
Thanks.
Code:
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
var cookieOptions = new CookieAuthenticationOptions();
cookieOptions.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie;
cookieOptions.LoginPath = new PathString("/Account/Login");
app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);
app.UseCookieAuthentication(cookieOptions);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = FranchiseAuthType,
ClientId = franchiseClientId,
Authority = FranchiseAuthority,
PostLogoutRedirectUri = postLogoutRedirectUri,
});
}

I'm not sure if you were able to resolve this issue, but what you want to do it is add
AuthenticationMode = AuthenticationMode.Passive
To your authentication options. This will make the OpenIdConnect authentication rely solely on your code to make calls to it. I believe this is what you intend to happen.
So your new code should look like this:
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
var cookieOptions = new CookieAuthenticationOptions();
cookieOptions.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie;
cookieOptions.LoginPath = new PathString("/Account/Login");
app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);
app.UseCookieAuthentication(cookieOptions);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = FranchiseAuthType,
AuthenticationMode = AuthenticationMode.Passive,
ClientId = franchiseClientId,
Authority = FranchiseAuthority,
PostLogoutRedirectUri = postLogoutRedirectUri,
});
}

EDIT: This looked like it fixed my problems but it caused a more serious issue than the one it fixed. If I set use cookies after use open Id connect then my openidconnect notifications will have a null sessions HttpContext.Current.Session and after authentication my authentication result is not stored in the cookie.
Whichever authentication is added last becomes the authoritative source for setting the redirect path for unauthenticated uses.
By moving
app.UseCookieAuthentication();
After app.UseOpenIdConnectAuthentication() the desired behavior was achieved.
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
var cookieOptions = new CookieAuthenticationOptions();
cookieOptions.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie;
cookieOptions.LoginPath = new PathString("/Account/Login");
app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = FranchiseAuthType,
ClientId = franchiseClientId,
Authority = FranchiseAuthority,
PostLogoutRedirectUri = postLogoutRedirectUri,
});
//move this after UseOpenIdConnectAuthentication and the LoginPath
//value is used for redirecting unauthenticated users
app.UseCookieAuthentication(cookieOptions);
}

Related

how to set up an asp.net mvc app for AD B2C "auth code flow" authentication

It states here
that going forward, we must prefer the auth code flow method since "With the plans for third party cookies to be removed from browsers, the implicit grant flow is no longer a suitable authentication method."
I have set up an asp.net mvc 4 web application according to the sample app here. It works when I set up my app registration in AD B2C directory for Access Tokens (used for implicit grant flows). If I switch to "ID Tokens (for implicit and hybrid flows)" as the documentation recommends, I get error that my application is not setup for it.
As I understand from the documentation, I would have to specifiy separate endpoins for /authorize and/token to fetch a token after authorization. I am not sure from looking at the sample though how exactly I can do this. Below is the ConfigureAuth method as you can see in the sample code on github link provided:
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(WellKnownMetadata, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}

Transient Infinite Login Loop on Azure App Service - OpenIDConnect Auth with Azure AD

Background
So we have an app service that authenticates from Azure AD in another tenancy using OpenIdConnect.
Login works on a dev instance of IIS, and it works on our test app service. We saw the issue on test, and it vanished and didn't return during the entire testing phase of the project.
Now we've deployed to production, and we're seeing the issue again.
The Issue
What we're seeing is that everything will work fine for some time, and then after several hours, the issue will emerge again.
We have a work around fix to restore service - that is to enable and then disable app service authentication in the azure control panel. The opposite works too - to disable and then enable will restore service.
The Code
public void ConfigureAuth(IAppBuilder app)
{
//Azure AD Configuration
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//sets client ID, authority, and RedirectUri as obtained from web config
ClientId = clientId,
ClientSecret = appKey,
Authority = authority,
RedirectUri = redirectUrl,
CallbackPath = new PathString("/"), //use this line for production and test
//page that users are redirected to on logout
PostLogoutRedirectUri = redirectUrl,
//scope - the claims that the app will make
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
//setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
//SaveSigninToken = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
}
}
);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
var code = context.Code;
ClientCredential cred = new ClientCredential(clientId, appKey);
string userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
//this token cache is stateful, we're going to populate it here, but we'll store it elsewhere in-case the user ends up accessing a different instance
AuthenticationContext authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));
// If you create the redirectUri this way, it will contain a trailing slash.
// Make sure you've registered the same exact Uri in the Azure Portal (including the slash).
Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, cred, "https://graph.windows.net");
//populate the persistent token cache
testdb2Entities5 db = new testdb2Entities5();
PersistentTokenCache tc = await db.PersistentTokenCaches.FindAsync(userObjectId);
//if null, populate a new item
if (tc == null)
{
tc = new PersistentTokenCache();
tc.object_id = userObjectId;
tc.token = code;
db.PersistentTokenCaches.Add(tc);
await db.SaveChangesAsync();
}
else
{
tc.token = code;
await db.SaveChangesAsync();
}
}
//authentication failed notifications
private Task OnAuthenticationFailed(AuthenticationFailedNotification<Microsoft.IdentityModel.Protocols
.OpenIdConnect.OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
The Question
So, based on whatever enabling and disabling app-service authentication does, it's clearly fixing thing temporarily. So I'm thinking this is a cookie related problem - as that'd be the only thing transferring state between sessions. What on earth could be the issue here? And what steps do I need to take to diagnose and resolve the issue?
So far, it seems like it was a problem with a known bug in Katana where the Katana cookie manager and the ASP .NET cookie manager clash and overwrite each other's cookies.
Here are some troubleshoots you could refer to:
1.Setting
app.UseCookieAuthentication(new CookieAuthenticationOptions {CookieSecure == CookieSecureOption.Always}). This means that cookie could leak along with your auth.
2.Add SystemWebCookieManager in UseCookieAuthentication which is in the Microsoft.Owin.Host.SystemWeb Nuget package. Please refer to this thread.
3.Split cookie. Someone noticed that it was issue if Cookie characters are more than browsers limit (> 4096). So to overcome that issue, in set-cookie with around 4000 characters with each and when needed combine all cookie together to get original value.
For more details about how to add sign-in with Microsoft to an ASP.NET web app, please refer to this article.
Update:
Fix with install Kentor.OwinCookieSaver nuget package and add app.UseKentorOwinCookieSaver(); before app.UseCookieAuthentication(new CookieAuthenticationOptions());

Is it possible to share AAD authentication cookie between two web applications on the same site

Question Is it possible for a back-end-web-app to accept and pick up the user identity from the AAD authentication cookie generated by signing in to a front-end-web-app?
Background I want to achieve an Azure Active Directory single sign on experience for a JavaScript SignalR client but my current solution requires the user to sign in twice. Once to access the web app hosting the js-client and then again so that the js-client-app can access the back-end.
Desired solution with SSO AAD Authentication
Front-end (OWIN/MVC/JavaScript)
AAD authentication to identify the user - implemented
JS SignalR client includes the auth cookie in the server requests - works by default
Back-end (OWIN/SignalR PersistentConnection)
Identify the user via the the encrypted .AspNetCookies cookie from the front-end authentication that is included in the SignalR requests.
the cookie is not accepted and the debug output from OWIN is Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware Warning: 0 : Unprotect ticket failed
Attempted Back-End Startup.cs
public void ConfigureAuth( IAppBuilder app )
app.SetDefaultSignInAsAuthenticationType( CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles"
}
});
}
Current solution with undesirable dual sign in
Front-end web app (OWIN/MVC/JavaScript)
Owin handles AAD authentication
MVC generates views based on the authenticated user
ADAL JS handles user authentication client-side and acquires an access token that is added as a cookie (my_access_token)
Back-end web app (OWIN/SignalR PersistentConnection)
OWIN handles authentication by extracting the access token found in the my-_access_token cookie and adding it as a Bearer Authorization header
Authorization is handled by overriding the Authorize function in PersistentConnection based on the authenticated user
Current Front-End Startup.cs
public void ConfigureAuth( IAppBuilder app )
GlobalFilters.Filters.Add( new AuthorizeAttribute() );
app.SetDefaultSignInAsAuthenticationType( CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles"
}
});
}
Current Back-end Startup.cs
public void Configuration( IAppBuilder app ){
app.Use( ( context, next ) =>
{
if (context.Request.Cookies.Any(c => c.Key.Equals("BearerToken")))
{
var cookie = context.Request.Cookies.First(c => c.Key.Equals("BearerToken"));
context.Request.Headers.Add("Authorization", new[] { $"Bearer {cookie.Value}" });
}
return next.Invoke();
});
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = tenant,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = audience,
RoleClaimType = "roles"
}
};
);
}
Yes, It's possible. You need to specify the shared key for cookie encryption & decryption.
https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-3.1
once your added AzureAd you just inject the shared cookie setting with the shared cookie name and the dataProtectionProvider from the same key in both apps.

ASP.net Identity stops handing out External Cookie after removing external account

I have a site setup with four 3rd-party login services, Microsoft, VS, Github, and Linkedin. Everything seems to work great, I can log in/out, add/remove external accounts with no problem.
Randomly however, it seems to stop working. When I try to login using any of the 3rd-party services, it just kicks me back to the login page.
Looking at the ExternalLoginCallback it appears that the AuthenticateResult.Identity is null and it can't get the external login info. Looking at it on the client-side it looks like they never got the external signin cookie.
I still can't consistently reproduce this error, so it's really hard to determine what might be happening. Any help would be great.
Update 1: I was able to identify the steps to reproduce:
Login to an account with more than 1 associated login
Remove one of the logins
In a new browser or a private session, try to log in with any of the 3rd-party accounts and you will be returned to login without an external cookie.
After hitting the error it won't hand out a cookie to any new sessions until IIS is restarted.
Update 2: Looks like it has something to do with setting a Session variable.
On the removeLogin action I was adding a value to the session. I'm not sure why but when I stopped doing that, I stopped having my problem. Time to figure out why... Update 3: Looks like this problem has been reported to the Katana Team
Update 4: Looks like someone else already ran into this problem. Stackoverflow post. They didn't give all of the code you needed to solve it, so I'll include that here as an answer.
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app) {
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(appContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationMode = AuthenticationMode.Active,
LoginPath = new PathString("/Login"),
LogoutPath = new PathString("/Logout"),
Provider = new CookieAuthenticationProvider {
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, User, int>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
)
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in with third party login providers
app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["MSA:Id"],
ClientSecret = ConfigurationManager.AppSettings["MSA:Secret"],
Caption = "Microsoft"
});
app.UseVisualStudioAuthentication(new VisualStudioAuthenticationOptions(){
AppId = ConfigurationManager.AppSettings["VSO:Id"],
AppSecret = ConfigurationManager.AppSettings["VSO:Secret"],
Provider = new VisualStudioAuthenticationProvider(){
OnAuthenticated = (context) =>{
context.Identity.AddClaim(new Claim("urn:vso:access_token", context.AccessToken, XmlSchemaString, "VisualStudio"));
context.Identity.AddClaim(new Claim("urn:vso:refresh_token", context.RefreshToken, XmlSchemaString, "VisualStudio"));
return Task.FromResult(0);
}
},
Caption = "Visual Studio"
});
app.UseGitHubAuthentication(new GitHubAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["GH:Id"],
ClientSecret = ConfigurationManager.AppSettings["GH:Secret"],
Caption = "Github"
});
app.UseLinkedInAuthentication(new LinkedInAuthenticationOptions {
ClientId = ConfigurationManager.AppSettings["LI:Id"],
ClientSecret = ConfigurationManager.AppSettings["LI:Secret"],
Caption = "LinkedIn"
});
}
OWIN and asp.net handle cookies/session differently. If you authorize with OWIN before you initialize a session, anyone after a session is initialized will not be able to login.
Workaround: Add the following to your Global.asax
// Fix for OWIN session bug
protected void Application_AcquireRequestState() {
Session["Workaround"] = 0;
}
}
Long term: The way OWIN and asp.net handle sessions/cookies will be merged in vNext, use the work around until then...

Why does Owin Startup order affect Cookie Authentication

I have Owin configured to issue both a token and cookie upon authentication:
public void Configuration(IAppBuilder app)
{
var cookieOptions = new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
CookieHttpOnly = true, // JavaScript should use the Bearer
//AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieName = "MyCookie",
LoginPath = new PathString("/app/index.html#/login"),
};
var oAuthServerOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new MyAuthorizationServerProvider(),
};
var oAuthBearerOptions = new OAuthBearerAuthenticationOptions
{
};
// Must be registered in this order!
app.UseCookieAuthentication(cookieOptions);
app.UseOAuthAuthorizationServer(oAuthServerOptions);
app.UseOAuthBearerAuthentication(oAuthBearerOptions);
}
This works fine - it issues both the Bearer token for my SPA to call my API and the cookie so my old school MVC pages can be logged in too.
But if I register the OAuth server before declaring that I want to use CookieAuth, no cookie is issued. In other words if I do this, it doesn't work:
app.UseOAuthAuthorizationServer(oAuthServerOptions);
app.UseCookieAuthentication(cookieOptions);
app.UseOAuthBearerAuthentication(oAuthBearerOptions);
Also, if I uncomment this line, it also doesn't issue the cookie:
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
My question is why does the order of registration with Owin matter? And why does setting the AuthenticationType in the cookie as "ApplicationCookie" also make it fail?
I'm not familiar with the UseOAuthAuthorizationServer() middleware, but I assume it works the same as other external authentication middleware (e.g. the Google middleware).
An authentication middleware that redirects to an external source for authentication, will only be used once, at the start, of each browsing session. It will then defer the authentication of the up-coming requests to a cookie authentication that maintains the session. This is good, because it means the overhead of the external authentication is only done once for each session.
A middleware that wants to set a cookie does typically not do it itself. Instead it sets a property in the Owin context with an AuthenticationResponseGrant. The grant is then processed by the cookie middleware that extracts the identity and sets the cookie.
For this to work:
The cookie handler must be registered before the external authentication middleware in the pipeline.
The authentication type in the AuthenticationResponseGrant must match the type of the cookie middleware.
So changing the order of the registration violates 1. and excluding the authentication type violates 2.
I have written an in depth blog post about it if you want more details.

Resources