OWIN WsFederation authentication with in-app authorization - asp.net-mvc

The scenario:
A ASP.NET MVC Web app for a company's internal users, configured for authentication against the company's ADFS, using Microsoft.Owin.Security.WsFederation
The company's ADFS contains several users, but only a few of them should be able to log in to the application.
I therefore have a database table containing these users' email addresses
The web app should check if the email claim received from ADFS exists in the DB table, and only issue a log in token for those users.
Other users should be redirected to a "Sorry you are not authorized to use this application"-page.
My question:
Where is the correct place to put the authorization logic that checks if an user should be allowed in?
Here's the code in my Startup.Configuration method:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
MetadataAddress = "https://.../FederationMetadata.xml",
Wtrealm = "...",
}
);

You have two options to achieve what you want:
1. One is to configure this at the very AD FS. I personally think this is the right way to do this, since the AD FS is the IdP and it should be the one controlling whether or not its users have access to the application. In this case the company should or should not allow somebody to use some of its resources (of course there are anti-arguments). This can be easily done at the Domain Controller, through the AD FS Management GUI. The following answer greatly describes this:
https://serverfault.com/a/676930/321380
2. Second is to use the Notifications object at the OWIN WSFed middleware in this way:
Notifications = new WsFederationAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
//extract claims' values and check identity data against your own authorization logic
bool isAuthorized = CheckForUnauthorizedAccess();
if (!isAuthorized)
{
throw new SecurityTokenValidationException("Unauthorized access attemp by {some_identifier}");
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
if (context.Exception is an unauthorized exception)
{
context.OwinContext.Response.Redirect("<unauthorized_redirect_url>");
}
context.HandleResponse(); // Suppress the exception
//exception logging goes here
return Task.FromResult(0);
}
}

Related

Initiate and store multiple OAuth2 external authentication challenges in a ASP.NET Core MVC application?

I can authenticate against two separate OAuth authentication schemes but it seems only one can be active at a time. I'd like to compare data from two separate SaaS applications and therefore I need two separate Bearer tokens. How can I initiate multiple OAuth challenges when the user loads the application and then store the Bearer Tokens for each? (e.g. in the Context.User cookie?)
My Startup.cs is as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddScheme1 (options =>
{
options.ClientId = Configuration["Scheme1:ClientId"];
options.ClientSecret = Configuration["Scheme1:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
})
.AddScheme2(options =>
{
options.ClientId = Configuration["Scheme2:ClientId"];
options.ClientSecret = Configuration["Scheme2:ClientSecret"];
options.Scope.Add("scope1");
options.SaveTokens = true;
});...
}
The AuthenticationController calls the Challenge overloaded method from the Microsoft.AspNetCore.Mvc.Core assembly that takes a single provider/scheme (passing multiple schemes in the overloaded method seems to be ignored).
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
...
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
Presumably, you'd prompt the user to sign-into one external application, redirect back to the home page, and then prompt them to sign-into the second one, and then allow them to start using the application proper.
If this is possible - e.g. using a "multiple" Auth cookie - how then would I fetch the correct Bearer token and User values for the given scheme? Currently you just seem to fetch the token with a generic "access_token" name and unique user values:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string userID = User.FindFirstValue(ClaimTypes.NameIdentifier);
There does seem to be some information here regarding using a SignInManager but I'm unable to determine if this is applicable to this problem.
I would aim to start with a standard architecture where the user authenticates with the one and only app, and gets only one set of tokens, issued by your own Authorization Server.
SaaS DATA - OPTION 1
Does the user need to get involved in these connections or can you use a back end to back end flow here?
Your C# code could connect to the SaaS provider with the client credentials grant, using the client ID and secret that you reference above. Provider tokens would then be cached in memory, then used by the back end code to return provider data to the UI. This is a simple option to code.
SaaS DATA - OPTION 2
If the user needs to get involved, because the data is owned by them, you might offer UI options like this. After each click the user is redirected again, to get a token for that provider.
View provider 1 data
View provider 2 data
Aim to emulate the embedded token pattern, where the provider tokens are available as a secondary credential. How you represent this could vary, eg you might prefer to store provider tokens in an encrypted cookie.
CODING AND SIMPLICITY
I would not mix up provider tokens with the primary OAuth mechanism of signing into the app and getting tokens via the .NET security framework, which typically implements OpenID Connect. Instead I would aim to code the SaaS connections on demand.
I think you will find it easier to code the SaaS connections with a library approach, such as Identity Model. This will also help you to deal with SaaS provider differences more easily.
I assume you use OIDC schemes.
First, you need to add two cookie schemes, one for each OIDC authentication scheme as their sign in scheme and set their callback path to different values to stop them competing:
services.AddAuthentication()
.AddCookie("Cookie1")
.AddCookie("Cookie2")
.AddOpenIdConnect("OidcScheme1", opt =>
{
opt.SignInScheme = "Cookie1";
opt.CallbackPath = "/signin-oidc-scheme1";
opt.SaveTokens = true;
})
.AddOpenIdConnect("OidcScheme2", opt =>
{
opt.SignInScheme = "Cookie2";
opt.CallbackPath = "/signin-oidc-scheme2";
opt.SaveTokens = true;
});
This will instruct the OIDC handler to authenticate the user from corresponding cookie.
Second, you need a controller action to challenge the user against each OIDC scheme:
[HttpGet]
[Route("login")]
[AllowAnonymous]
public IActionResult Login([FromQuery]string scheme,
[FromQuery]string? returnUrl)
{
return Challenge(new AuthenticationProperties
{
RedirectUri = returnUrl ?? "/"
}, scheme);
}
From your web app, you need to send the user to the Login endpoint twice with different scheme values:
GET /login?scheme=OidcScheme1
GET /login?scheme=OidcScheme2
Or chain them together using the returnUrl:
GET /login?scheme=OidcScheme1&returnUrl=%2Flogin%3Fscheme%3DOidcScheme2
Once signed in, there should be two cookies in the browser window, for example:
To authenticate the user and restore both identities from two cookies, you can use authorization policy:
[HttpGet]
[Authorize(AuthenticationSchemes = "OidcScheme1,OidcScheme2")]
public async Task<IActionResult> SomeOperation()
{
// Two identities, one from each cookie
var userIdentities = User.Identities;
...
}
To get access token from each authentication scheme, use the method you discovered (GetTokenAsync) and specify authentication scheme:
var token1 = await HttpContext.GetTokenAsync("OidcScheme1", "access_token");
var token2 = await HttpContext.GetTokenAsync("OidcScheme2", "access_token");
It is possible that the access token is not returned from the token endpoint depends on the response_type you used. If this is the case, try set the OpenIdConnectionOptions.ResponseType to OpenIdConnectResponseType.Code and make sure the scope is correct.
I encountered a similar problem where we had microservices that are/were shared across multiple products with each product having a separate IDP tenant (essentially a different token issuer). Perhaps a similar approach might work for your scenario...
The following link helped me with a solution - see here.
Basically I defined a smart authentication scheme
var builder = services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = "smart";
//...
});
Then in the smart policy scheme definition, I decode the JWT coming in to work out the issuer from the iss claim in the JWT, so that I can forward to the correct location for JWT bearer authentication.
builder.AddPolicyScheme("smart", "smart", options =>
{
options.ForwardDefaultSelector = context =>
{
var jwtEncodedString = context.Request.Headers["Authorization"].FirstOrDefault()?.Substring(7);
if (string.IsNullOrEmpty(jwtEncodedString))
return settings.Tenants.First().Key; // There's no authorization header, so just return any.
var token = new JwtSecurityToken(jwtEncodedString: jwtEncodedString);
var issuer = token.Claims.First(c => c.Type == "iss").Value?.TrimEnd('/');
var tenant = settings.Tenants
.Where(pair => pair.Value.Issuer.TrimEnd('/') == issuer)
.Select(pair => pair.Key).FirstOrDefault();
if (tenant == null)
throw new AuthorizationException($"Failed to locate authorization tenant with issuer '{issuer}'.");
return tenant;
};
});
Note: settings.Tenants is just an array of whitelisted tenants (from appsettings) that I configure as follows:
foreach (var tenant in settings.Tenants)
builder.AddJwtBearer(tenant.Key, options => Configure(options, tenant.Value, defaultJwtBearerEvents));

How does ADFS keep a user authenticated?

I'm using WsFed to implement ADFS SSO into an app. If I try to run [Authorize] methods, I'm taken to the sign in page. When I sign in, a cookie with encrypted information is created and I'm able to run [Authorize] methods. The cookie has option ExpireTimeSpan = TimeSpan.FromSeconds(10);. So far, this works as expected and an unauthorized user cannot access the app.
The confusion begins when the cookie expires, is altered, or deleted from the browser. When this happens, if I run an [Authorized] method I'm automatically signed in again without needing to reenter my credentials and the cookie is recreated. However, if I explicitly sign out using return SignOut(... method, then I am required to reenter my credentials.
Why does ADFS re-authenticate me if I delete the cookie, and how does it know to do so? It doesn't do it if I explicitly sign out. Shouldn't remaining authenticated depend on the cookie being present with the correct values?
Authentication setup in Startup.ConfigureServices:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = Configuration["AppSettings:wsfed:realm"];
options.MetadataAddress = Configuration["AppSettings:wsfed:metadata"];
options.UseTokenLifetime = false;
})
.AddCookie(options =>
{
options.Cookie.Name = "AuthenticationCookie";
options.LoginPath = "/signin-wsfed";
options.LogoutPath = "/NameController/Logout";
options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
options.SlidingExpiration = true;
});
Login action:
[AllowAnonymous]
[HttpGet]
public IActionResult Login()
{
var authProperties = new AuthenticationProperties
{
RedirectUri = "https://app:1234/NameController/Index",
};
return Challenge(authProperties, WsFederationDefaults.AuthenticationScheme);
}
Logout action:
[AllowAnonymous]
[HttpGet]
public IActionResult SignOutOfADFS()
{
return SignOut(
new AuthenticationProperties
{
RedirectUri = "https://app:1234/NameController/AfterLogout"
},
CookieAuthenticationDefaults.AuthenticationScheme,
WsFederationDefaults.AuthenticationScheme);
}
The AD FS is an identity provider that is commonly used for single sign-on purposes. As part of that, a key feature is that the AD FS does remember the signed-in user in order to authenticate them for another website. It does that by remembering the user using a separate session persisted using a cookie for the AD FS website.
When you sign out locally from your application, then all you are doing is clearing your local cookie. So when you try to authenticate again and the user is challenged to authenticate with the identity provider, the AD FS is able to sign the user in without asking them again for their credentials. For the AD FS your application is then just like a third website which is asking for authentication after the user already signed in into the AD FS.
In order to sign out completely, you will have to do a WSFederation sign out. As part of that process, the local cookie is cleared and then the user is redirected to an AD FS signout page where the AD FS authentication cookie is also cleared. On a subsequent authentication attempt, the AD FS then cannot remember the user anymore (since there’s no cookie) so they have to authenticate again with their credentials. That is what you are doing in your SignOutOfADFS action.
The WSFederation protocol supports a way for the authenticating application to require the user to reauthenticate with the identity provider by passing the wfresh=0 parameter with the authentication request. This is also supported in current AD FS versions. Unfortunately, I don’t think this parameter is currently supported by the WSFederation authentication handler for ASP.NET Core. It wouldn’t really prevent the user from reusing their authentication though, so you wouldn’t be able to use this a security feature.

AuthorizationCodeReceived event not firing

I'm attempting to implement the OpenId Connect middleware in a an ASP.NET MVC 5 (.Net Framework) application.
In my AccountController.cs I send an OpenID Connect sing-in request. I have another OpenId connect middleware implemented which is why I specify that the middleware I want to challenge against is "AlternateIdentityProvider".
public void SignIn()
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
"AlternateIdentityProvider");
}
Upon issuing a challenge against the middleware, the RedirectToIdentityProvider event in Startup.cs fires and I am redirected to the provider for sign in. However, after successfully signing in I am redirected to the specified redirect uri with the state and code parameters added as query parameters i.e. http://localhost:63242/singin-oidc/?state=State&code=AuthorizationCode (parameters removed for brevity), which results in a 404 as no such route exists in my application.
Instead I expected the successful signin to trigger the AuthorizationCodeReceived event where I can implement my additional logic. In fact none of the other events ever trigger.
I have implemented an almost identical solution in ASP.Net Core 2.1 and here I am able to step through the different events as they trigger.
The relevant code of my current Startup.cs is shown below. Note that the OpenId provider throws an error if the inital request include reponse_mode and some telemetry parameters, hence these are removed during the initial RedirectToIdentityProvider event.
Any ideas why the callback from the OpenId provider is not getting picked up in the middleware?
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions("AlternateIdentityProvider")
{
ClientId = { { Client Id } },
ClientSecret = { { Client Secret } },
Scope = OpenIdConnectScope.OpenId,
ResponseType = OpenIdConnectResponseType.Code,
RedirectUri = "http://localhost:63242/singin-oidc",
MetadataAddress = { { Discovery document url } },
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = context =>
{
Debug.WriteLine("Redirecting to identity provider for sign in..");
context.ProtocolMessage.EnableTelemetryParameters = false;
context.ProtocolMessage.ResponseMode = null;
return Task.FromResult(0);
},
AuthorizationCodeReceived = context => {
Debug.WriteLine("Authorization code received..");
return Task.FromResult(0);
},
SecurityTokenReceived = context =>
{
Debug.WriteLine("Token response received..");
return Task.FromResult(0);
},
SecurityTokenValidated = context =>
{
Debug.WriteLine("Token validated..");
return Task.FromResult(0);
},
}
});
I was encountering the same issue. I am trying to plug in Owin into our legacy WebForms app.
For me, I had to do the following:
1) Change the application manifest of the application definition on Azure to set the "oauth2AllowIdTokenImplicitFlow" property to true from false.
Go to the Azure Portal,
Select to Azure Active Directory
Select App Registrations
Select your app.
Click on Manifest
Find the value oauth2AllowIdTokenImplicitFlow and change it's value to true
Click Save
2) In your startup.cs file, change the following:
ResponseType = OpenIdConnectResponseType.Code
to
ResponseType = OpenIdConnectResponseType.CodeIdToken
Once, I did those two things, the SecurityTokenValidated and AuthorizationCodeReceived started firing.
Though, I am not sure this is the right way to go or not. Need to do more reading.
Hope this helps.
Please be aware that the OpenId Connect implementation in .Net Framework only support response_mode=form_post. (See closed GitHub issue)
Since you strip the parameter in the request to the OpenId Connect provider (in your RedirectToIdentityProvider notification), then the provider will default to response_mode=query pr. the specs. (see relation between response_type and response_mode ind the specs.)
So in short the OpenId Connect middleware expects that there will come a HTTP POST (with a form body) and your provider will properly send a HTTP GET (with parameters as query-string).

OpenID Connect ASP NET MVC AAD

I implemented a sample app using OpenID Connect standard with ASP NET MVC website. My goal was to outsource storing sensitive data to Azure so i used Azure Active Directory. Since it's impossible to add custom properties to users in Azure i store non sensitive user Claims in our private db. I managed to get this claims and "add" them to the cookie like this:
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = context =>
{
var objectId = context.AuthenticationTicket.Identity.Claims.First(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier");
var claims = GetUserClaims(objectId.Value);
foreach (var item in claims)
{
context.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim(item.Key, item.Value));
}
return Task.FromResult(0);
}
}
This way I added required claims to the cookie so those claims persist in my User object until user sign-out which is fine but there is one Claim which can change during the session ( basically user can change it on one page ). The problem is I can't find how to "change" this Claim in the cookie so it will persist. Ideally I would like to somehow force
AuthorizationCodeReceived
function to be called again. Is it possible ? Or is there another way where I can swap the value stored in the cookie ?
So far my only solution is to log-out user when he change this value so it will force him to sign-out again and my callback for AuthorizationCodeReceived will be called again, but it's not a very user-friendly way.
You can call HttpContext.GetOwinContext().Authentication.SignIn() after you add a claim in identity object to persist the new claim in cookie.

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...

Resources