My ASP.Net app uses OWIN/Katana/Claims, and allows login using:
Traditional username/password (exists for all users)
Google
Azure AD
It works perfectly, and all the necessary redirects/claims transfers work well (the user NameIdentifier/Provider(/tenant) details are passed back to my app so the unique id values can be linked up). Note that users do not sign up/register for the app - access is provisioned by their organisation's super-user, and a username/password sent to them which they can then hook up with Google/Azure.
However, I now need to extend this functionality to allow users to hook up to their organisation's ADFS provider. The only working example for this that's remotely close is here (tutorial/code), but it is strictly based on ADFS-only. When I adapt this into my project, it doesn't work.
My entire StartupAuth file is shown below. I appreciate that there may be configuration errors, but based on the scraps of samples I've found over the last six weeks, this is the best I've had.
public void Configuration(IAppBuilder app)
{
// STANDARD CODE FOR APP COOKIE AND GOOGLE - WORKS PERFECTLY
CookieAuthenticationOptions coa = new CookieAuthenticationOptions {
AuthenticationMode = AuthenticationMode.Active,
CookieName = "MyAppName",
ExpireTimeSpan = TimeSpan.FromMinutes(60),
SlidingExpiration = true,
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/login.aspx"),
CookieHttpOnly = true,
CookieSecure = CookieSecureOption.SameAsRequest,
Provider = new CookieAuthenticationProvider { OnValidateIdentity = context =>
{
dynamic ret = Task.Run(() =>
{
// Verify that "userId" and "customerId" claims exist, and that each has a valid value (greater than zero) - removed for brevity
return Task.FromResult(0);
});
return ret;
} }
};
app.SetDefaultSignInAsAuthenticationType(coa.AuthenticationType);
app.UseCookieAuthentication(coa);
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions {
ClientId = "84***********************k3.apps.googleusercontent.com",
ClientSecret = "jue*****************Ppi"
});
// NEW CODE THAT FAILS TO WORK - SPECIFYING EACH CUSTOMER'S ADFS AS A NEW WSFED AUTH OPTION
WsFederation.WsFederationAuthenticationOptions Adfs_CompanyA = new WsFederation.WsFederationAuthenticationOptions {
AuthenticationMode = AuthenticationMode.Passive,
MetadataAddress = "https://CompanyA.net/FederationMetadata/2007-06/FederationMetadata.xml",
AuthenticationType = AdfsAuthenticationTypes.CompanyA,
Wtrealm = "https://www.CompanyA.co.uk/MyAppName"
};
WsFederation.WsFederationAuthenticationOptions Adfs_CompanyB = new WsFederation.WsFederationAuthenticationOptions {
AuthenticationMode = AuthenticationMode.Passive,
MetadataAddress = "https://CompanyB.net/federationmetadata/2007-06/federationmetadata.xml",
AuthenticationType = AdfsAuthenticationTypes.CompanyB,
Wtrealm = "http://www.CompanyB.co.uk/azure/MyAppName"
};
// User (who is logged in), route for hyperlink "Link my account with ADFS"
app.Map("/SSO/LinkUserAccount/ADFS/process", configuration => { configuration.UseWsFederationAuthentication(Adfs_CompanyA); });
// CompanyA ADFS - single sign-on route
app.Map("/SSO/Login/CompanyA/ADFS/Go", configuration => { configuration.UseWsFederationAuthentication(Adfs_CompanyA); });
// CompanyB ADFS - single sign-on route
app.Map("/SSO/Login/CompanyB/ADFS/Go", configuration => { configuration.UseWsFederationAuthentication(Adfs_CompanyB); });
}
}
Here is the code I use to issue an OWIN Challenge:
string provider = MyApp.SingleSignOn.GetCustomerAdfsAuthenticationType(customerName);
string redirectUrl = string.Format("{0}/SSO/Login/{1}/ADFS/Go", Request.Url.GetLeftPart(UriPartial.Authority), provider); // creates https://myapp.com/SSO/Login/CompanyA/ADFS/Go for CompanyA users
Context.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = redirectUrl }, provider);
Response.StatusCode = 401;
Response.End();
This is webforms, but please don't let that stop MVC pro's from contributing. The code is virtually identical anyway and I'm using routes.
The problem I have is that when the user clicks on the "sign-on with ADFS" link, e.g. URL becomes https://myapp.com/SSO/Login/CompanyA/ADFS
I get an 401 Unauthorized error, instead of the user being redirected to the ADFS login page.
In web.config, I allow unauthorized access to path "SSO". For some reason the Challenge() method never redirects the user, it simply gets ignored and the code reaches the point where it returns a 401. The value of string provider exactly matches the WsFederationAuthenticationOptions.AuthenticationType value defined in Startup.Auth.
I've been sturggling with this now for six weeks, so this is getting a bounty at the first opportunity, and a crate of beer delivered to your chosen address when it is solved.
I solved the problem. Amazingly, it was as simple as me missing this from the end of StartupAuth:
app.UseStageMarker(PipelineStage.Authenticate);
Have you set up OWIN logging? Any clues there?
Also Test driving the WS-Federation Authentication Middleware for Katana.
Have a look at the code in IdentityServer 3. There's a WS-Fed plugin there and the documentation is here (at the bottom).
Related
I have read and followed this article to setup my site using our AAD (Azure Active Directory) to get SSO (Single Sign On.) I have gotten it to work in a brand new website both with localhost as well as when I publish it to Azure.
Here are the settings for the working version's App Registration:
Branding:
Home page URL: https://<worksgood>.azurewebsites.net
Authentication:
Redirect URIs:
https://localhost:44390/
https://<worksgood>.azurewebsites.net/.auth/login/aad/callback
Implicit grant:
ID Tokens: Checked
Supported account types
Accounts in this organizational directory only (My Company - Single tenant)
Treat application as a public client
No
And when I run the application here is the callback request.
As you can see the Response Header | Location looks good (to me)
Here are the App Registration settings for the site I am attempting to integrate this same logic into:
Branding:
Home page URL: https://<notsogood>.azurewebsites.net
Authentication:
Redirect URIs:
https://localhost:54449/
https://<notsogood>.azurewebsites.net/.auth/login/aad/callback
Implicit grant:
ID Tokens: Checked
Supported account types
Accounts in this organizational directory only (My Company - Single tenant)
Treat application as a public client
No
And when I run the application here is the callback request.
When I run it, I do get the AD login screen where I enter my AD user and creds. However, it does not successfully log me in.
As you can see, the Location in the response gets altered. I do know that this non-working version has the authentication and authorization sections within the web.config and if I change the loginUrl attribute from /login to /loginklg it will change the location to /loginklg?ReturnUrl=%2f.auth%2flogin%2faad%2fcallback but if I remove that section the site will not work.
You should also notice that there is a loop where it attempts to log me in and then for some reason can not and then tries again.
Initially, the not working site had the following startup code for authentication:
public void ConfigureAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/login"),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)
)
}
});
}
I have kept this in as well as taken it out and it makes no difference in my result.
The only real difference is that the working version is MVC and the SignIn method is called.
public void SignIn() {
if (!Request.IsAuthenticated) {
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
And with the not working version it is a WebForm/Page and the Page_Load method is called:
Please Note
This application was not created by me nor my company, so I am trying to integrate it with simply some separate classes and config settings with the least code change as possible. The _avantiaSSOEnabled just reads an appSettings in the web.config which I added. The _openIdEnabled already existed.
_openIdEnabled = false
_avantiaSSOEnabled = true
Even if I enable _openIdEnabled the Location is still bad.
protected void Page_Load(object sender, EventArgs e) {
if (_avantiaSSOEnabled) {
if (!Request.IsAuthenticated) {
Request.GetOwinContext().Authentication.Challenge(
new Microsoft.Owin.Security.AuthenticationProperties { RedirectUri = "/klg" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
if (_openIdEnabled)
openIdBackgroundSignIn.OnOpenIdSSOLoggedIn += OnOpenIdSSOLoggedIn;
if (!IsPostBack) {
if (SystemHub.Maintenance.IsActive)
HandleInfoPopup(MaintenenceException.Text, true);
else if (Request["error"] != null)
HandleError(Request["error"].ToString());
else if (Request["auto"] == "true")
HandleInfoPopup(AutoLogout.Text, true);
else if (_openIdEnabled) {
openIdBackgroundSignIn.ClearData();
if (Request["oidc_error"] != null) //This is usually when auto-login fails, so we pass it to client side which will handle it
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ERROR, Request["oidc_error"].ToString());
else if (Request["oidc_login"] == "true")
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_LOGIN_SUCCESS, true);
else if (User.Identity.IsAuthenticated)
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
else if (Request["lo"] == null) //lo is set when coming from logout, so don't try to autologin
openIdBackgroundSignIn.AddData(OpenIdBackgroundSignIn.OPENID_KEY_ATTEMPT_LOGIN_AUTO, true);
}
else if (User.Identity.IsAuthenticated) {
Response.RedirectToUrl(Request.QueryString["ReturnUrl"]);
}
}
}
The only code change I made (that wasn't in the above linked article) was in a attempt to fix it, reading many other articles and there is a known issue with default cookie manager. Here is the result:
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager() // Originally SystemWebCookieManager
});
I know I am close. Clearly something is intercepting the request and tweaking it. I am just not sure where to look. I have been coding in C# since the start, but I am not that used to the security/SSO side of it, so any help is appreciated. If you need me to add more information, I can, just let me know what.
UPDATE - 07/31/2020
I was able to fix the Location /login?ReturnUrl... after following this article as you can see below.
As you can see in the image below, from the AAD Sign-in Log, I am successfully logging in. So it seems as if the code is unable to remember or store the token once logged in and then just tries again and must have some threshold of tries or time before it fails.
Oddly enough, when it stops looping I get the following message which has the email of the account I am attempting to log in as and says "Signed in"
The looping issues was fixed by removing the following line:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
The looping was happening because the authentication type (set in the above line) would return Cookies. However, the response from AAD was setting the type as ApplicationCookie.
The full code in the ConfigAuth is now:
public void ConfigAuth(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions {
CookieManager = new SystemWebChunkingCookieManager(),
Provider = new CookieAuthenticationProvider {
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: AuthenticationHelper.OpenIdEnabled
? TimeSpan.FromSeconds(30)
: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
ClientId = AvantiaSSOHelper.ClientId,
Authority = AvantiaSSOHelper.Authority,
PostLogoutRedirectUri = AvantiaSSOHelper.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = (context) => {
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
},
AuthorizationCodeReceived = (context) => {
Debug.WriteLine($"Authorization code received: {context.Code}");
return Task.FromResult(0);
},
MessageReceived = (context) => {
Debug.WriteLine($"Message received: {context.Response.StatusCode}");
return Task.FromResult(0);
},
SecurityTokenReceived = (context) => {
Debug.WriteLine($"Security token received: {context.ProtocolMessage.IdToken}");
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) => {
Debug.WriteLine($"Security token validated: {context.Response.StatusCode}");
var nameClaim = context.AuthenticationTicket.Identity.Claims
.Where(x => x.Type == AvantiaSSOHelper.ClaimTypeWithEmail)
.FirstOrDefault();
if (nameClaim != null)
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
return Task.FromResult(0);
},
TokenResponseReceived = (context) => {
string test = context.ProtocolMessage.AccessToken;
return Task.FromResult(0);
}
}
}
);
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
That made a single (non-looping) call and then the system attempted to continue in an Authenticated mode. However, there was still one more step I needed to do. This last step was to alter the SecurityTokenValidated event by adding the appropriate response claim into the authentication ticket. Our system is using Micrososft Identity and is thus based on an email address. So I need to add a Claim of type ClaimTypes.Name to the authentication ticket from the extracted email claims value as follows:
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, nameClaim.Value));
The AvantiaSSOHelper.ClaimTypeWithEmail is simply a value I am reading out of the Web.config file in case other implementations have a different claim I would need to extsract.
We have a ASP.NET application that uses Microsoft's common login to log in the users, then it redirects back to our web application (in Azure). The authentication is connected to Azure Active Directories. It is a multi-tenant application with multiple Azure ADs. When Microsoft redirects back to our site, we use the information to create a cookie that is used for the web calls. In addition, the call back returns a user code that we use to get a token. This is used as authentication against our API controllers.
This has been working for a long time. Now, however, we need to integrate with another 3rd party portal that will launch our product. They will be using SAML for SSO. They are not integrated with Azure AD. So, the idea is that we validate the users via the SAML assertions. That will contain the username that we then want to "log in" with.
I can create the cookie off of this information and that works fine with our web controller calls. However, since I'm not getting a callback from Azure AD, I don't have the token for the API calls. I have tried to call Azure AD to authenticate the applications, but that does seem to satisfy the API Controller's authorization. Specifically, RequestContext.Principal.Identity doesn't seem to be set with this pattern.
I have set the cookie authentication with this code:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
var cookieOptions = new CookieAuthenticationOptions
{
ExpireTimeSpan = TimeSpan.FromDays(14),
SlidingExpiration = true,
LoginPath = new PathString("/home/login"),
AuthenticationType = "ApplicationCookie",
AuthenticationMode = AuthenticationMode.Active,
CookieHttpOnly = true,
CookieSecure = CookieSecureOption.Always,
CookieSameSite = SameSiteMode.Lax,
};
// Forms/Cookie Authentication
app.UseCookieAuthentication(cookieOptions);
And I left the bearer token authentication code like this:
// Bearer Token Authentication used for API access
BearerAuthenticationOptions = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = <application tenant id>,
AuthenticationType = OAuthDefaults.AuthenticationType,
// Disable Issuer validation. We'll validate the Isuuer in the ClaimsAuthorizationFilter.
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = <resource id>,
ValidateIssuer = false
},
};
app.UseWindowsAzureActiveDirectoryBearerAuthentication(BearerAuthenticationOptions);
The code that handles the Azure AD auth (that the SAML login should replace) is:
var openIdConnectOptions = new OpenIdConnectAuthenticationOptions { ClientId = <ClientId>, Authority = "https://login.windows.net/common/", // setting this to false uses the cookie expiration instead UseTokenLifetime = false, TokenValidationParameters = new TokenValidationParameters { // we'll validate Issuer on the SecurityTokenValidated notification below ValidateIssuer = false },
Notifications = new OpenIdConnectAuthenticationNotifications { // An AAD auth token is validated and we have a Claims Identity SecurityTokenValidated = context => { ... additional validation is performed here...
return Task.FromResult(0); },
//the user has just signed in to the external auth provider (AAD) and then were redirected here // with an access code that we can use to turn around and acquire an auth token AuthorizationCodeReceived = context => { var code = context.Code; var identity = context.AuthenticationTicket.Identity;
var appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; // causes the retreived API token to be cached for later use TokenService.GetUserLevelTokenFromAccessCode(new HttpUserSessionWithClaimsId(identity), code, <ApiResourceId>, new Uri(appBaseUrl));
return Task.FromResult(0); }, // We are about to redirect to the identity provider (AAD) RedirectToIdentityProvider = context => { // This ensures that the address used for sign in and sign out is picked up dynamically from the request // Remember that the base URL of the address used here must be provisioned in Azure AD beforehand. var appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; context.ProtocolMessage.RedirectUri = appBaseUrl + "/"; context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
context.HandleResponse();
return Task.FromResult(0); }, // Something went wrong during this auth process AuthenticationFailed = context => { if (context.Exception is Microsoft.IdentityModel.Protocols.OpenIdConnect
.OpenIdConnectProtocolInvalidNonceException) {
//This is a message we can't do anything about, so we want to ignore it.
Log.Info("AuthenticationFailed in OpenIdConnectAuthentication middleware", context.Exception); } else {
Log.Error("AuthenticationFailed in OpenIdConnectAuthentication middleware",
context.Exception); }
// IDX10205 == Tenant validation failed var message = (context.Exception.Message.StartsWith("IDX10205"))
? InvalidTenantMessage
: GenericErrorMessage;
context.OwinContext.Response.Redirect("/?Error=" + Uri.EscapeDataString(message)); context.HandleResponse(); // Suppress the exception return Task.FromResult(0); }, MessageReceived = context => { if (!string.IsNullOrWhiteSpace(context.ProtocolMessage.Error)) {
// AADSTS65004 == user did not grant access in OAuth flow
Log.Error("MessageReceived containing error in OpenIdConnectAuthentication middleware. \nError: {0}\nDescription: {1}"
.FormatWith(context.ProtocolMessage.Error, context.ProtocolMessage.ErrorDescription));
//context.OwinContext.Response.Redirect("/");
//context.HandleResponse(); // Suppress the exception } return Task.FromResult(0); } } };
app.UseOpenIdConnectAuthentication(openIdConnectOptions);
Any help would be greatly appreciated.
Turns out the value I put for AuthenticationType in the cookie was causing the disconnect. Once I fixed that, the data came through. So closing his question.
Thanks.
In the Azure B2C, I used to be able to get a "groups" claim in my JWT tokens by following Retrieving Azure AD Group information with JWT:
Open the old-school Azure manager (https://manage.windowsazure.com)
Register my application with B2C
Download the B2C manifest for the application
In the manifest, change the "groupMembershipClaims" entry to
"groupMembershipClaims": "SecurityGroup",
Upload the changed B2C manifest again
The problem
This has worked well in the past (about a month ago, I believe...) but it doesn't anymore. See below for details...
What I've tried sofar
Plan A: Use Azure Manager
Follow the known-good recipe above.
Unfortunately that doesn't work anymore - I get the following error when this client tries to authenticate me with B2C:
AADB2C90068: The provided application with ID '032fe196-e17d-4287-9cfd-25386d49c0d5' is not valid against this service. Please use an application created via the B2C portal and try again"
OK, fair enough - they're moving us to the new Portal.
Plan B: Use Azure Portal
Follow the good old recipe, using the new Portal.
But that doesn't work either - when I get to the "download manifest" part, I cannot find any way to access the manifest (and Googling tells me it's probably gone for good...).
Plan C: Mix Azure Portal and manager
Getting a little desperate, I tried mixing plans A and B: register the app using the new Portal, then change the manifest using the old Azure Manager.
But no luck - when I try to upload the manifest, it fails with the message
ParameterValidationException=Invalid parameters provided; BadRequestException=Updates to converged applications are not allowed in this version.
Plan Z: Use the Graph API to retrieve group membership data
Just give up the "group" claim - instead, whenever I need group info, just query the B2C server using the Graph API.
I really, really don't want to do this - it would ruin the self-contained-ness of the access token, and make the system very "chatty".
But I've included it as a plan Z here, just to say: yes, I know the option exists, no I haven't tried it - and I'd prefer not to.
The question:
How do I get the "group" claim in my JWT token these days?
Plan Z it is I'm afraid. I don't know why they don't return it, but it's currently marked as planned on their Feedback Portal (it's the highest rated item).
This is how I'm doing it. Querying the groups when the user is authenticated, you can do it your way as well - just query as and when you need to. Depends on your use case.
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = new PathString("/account/unauthorised"),
CookieSecure = CookieSecureOption.Always,
ExpireTimeSpan = TimeSpan.FromMinutes(20),
SlidingExpiration = true,
CookieHttpOnly = true
});
// Configure OpenID Connect middleware for each policy
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(Globals.SignInPolicyId));
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = string.Format(Globals.AadInstance, Globals.TenantName, policy),
AuthenticationType = policy,
AuthenticationMode = AuthenticationMode.Active,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientIdForLogin,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = AuthenticationFailed,
SecurityTokenValidated = SecurityTokenValidated
},
Scope = "openid",
ResponseType = "id_token",
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
}
};
}
private async Task SecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> token)
{
var groups = await _metaDataService.GetGroups(token.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value);
if (groups?.Value != null && groups.Value.Any())
{
foreach (IGroup group in groups.Value.ToList())
{
token.AuthenticationTicket.Identity.AddClaim(
new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String, "GRAPH"));
}
}
}
// Used for avoiding yellow-screen-of-death
private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
}
My GetGroups method just queries the getMemberGroups method on the Users API
Then I have a simple helper method to determine whether the user is in a role:
public static bool UserIsInRole(IPrincipal user, string roleName)
{
var claims = user.Identity as ClaimsIdentity;
if (claims == null) return false;
return claims.FindAll(x => x.Type == ClaimTypes.Role).Any(x => x.Value == roleName);
}
I'm struggling to understand how IdentityServer3, AzureAD and a Private Database all work together. The biggest problem is how the Redirect URIs are being handled.
My scenario is I have a stand alone IdentityServer3. It's job is to authenticate users against either AzureAD or a private DB. Within the Startup.cs file on the ID3 server, I have the following OpenID Connect code:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", s3App =>
{
s3App.UseIdentityServer(new IdentityServerOptions
{
SiteName = "3S",
SigningCertificate = Certificate.Load(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(InMemoryUsers.Get())
.UseInMemoryClients(InMemoryClients.Get())
.UseInMemoryScopes(InMemoryScopes.Get()),
AuthenticationOptions = new AuthenticationOptions
{
EnablePostSignOutAutoRedirect = true,
EnableSignOutPrompt = false,
IdentityProviders = ConfigureAdditionalIdentityProviders
}
});
});
}
public static void ConfigureAdditionalIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "AzureAd",
Caption = "Login",
ClientId = "4613ed32-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // GUID of registered application on Azure
Authority = "https://login.microsoftonline.com/our-tenant-id/",
PostLogoutRedirectUri = "https://localhost:44348/identity",
RedirectUri = "https://localhost:44348/identity",
Scope = "openid email profile",
ResponseType = "id_token",
AuthenticationMode = AuthenticationMode.Passive,
SignInAsAuthenticationType = signInAsType,
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = Constants.ExternalAuthenticationType,
ValidateIssuer = false
}
});
}
I don't understand why the ID3 Server would need to have either RedirectUri or PostLogoutRedirectUri...shouldn't that be "passed through" from the application requesting the authentication? After all, we want to get back to the application, not the ID3 Server. Granted, I don't think this is what's causing my problem, but it would be nice to understand why these are here.
I will say, I've gotten "close" to this working.
When my application requiring authentication requests authentication against AzureAD, I'm redirected to the Microsoft Account login screen to enter my username/password for my work account. I submit my credentials and then get redirected back to either the ID3 server or my application, depending on which RedirectUri has been used in the above code.
For the sake of argument, let's say I use my application for the RedirectUri. I will be sent back to the application, but not to the page that initially prompted the authentication challenge, and if I click on a page that requires authentication, I'm sent back to the AzureAD server to log in again, only this time AzureAD recognizes me as already logged in.
Unfortunately, it doesn't appear that the SecurityTokenValidated notification is being acknowledged/set after the redirect from AzureAD.
Here's the code found in the application Startup.cs:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44348/identity",
ClientId = "3af8e3ba-5a04-4acc-8c51-1d30f8587ced", // Local ClientID registered as part of the IdentityServer3 InMemoryClients
Scope = "openid profile roles",
RedirectUri = "http://localhost:52702/",
PostLogoutRedirectUri = "http://localhost:52702/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
var sub = id.FindFirst(Constants.ClaimTypes.Subject);
var roles = id.FindAll(Constants.ClaimTypes.Role);
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role
);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaim(sub);
nid.AddClaims(roles);
nid.AddClaim(new Claim("application_specific", "Some data goes here. Not sure what, though."));
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(nid, n.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
return Task.FromResult(0);
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
context.Response.Redirect("/Error/message=" + context.Exception.Message);
//Debug.WriteLine("*** AuthenticationFailed");
return Task.FromResult(0);
},
}
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
}
}
You'll notice that the OpenIdConnectAuthenticationOptions also contain a RedirectUri and a PostLogoutRedirectUri which point to the application, but those don't seem to matter.
Of course, everything works perfectly when I'm logging in using the "cookies" side of things - I see all of my claims for the user. And, spending some time on the phone with Microsoft, they proposed a solution outside of ID3 which worked, but is not the way we need to go. We will have multiple applications authenticating against our ID3 so we need to contain and control the flow internally.
I really need some help trying to figure out this last mile issue. I know I'm close, I've just been staring at this so long that I'm probably staring right at my error and not seeing it.
10/22/2016 Edit
Further testing and enabling Serilog revealed an issue with the RedirectUri and PostLogoutRedirectUri resulted in my adding the /identity to the end of the URIs which corresponds to the value set in app.Map. This resolved the issue of my being returned to the "blank" page of IdentityServer3, I'm now returned to the IdentityServer3 login screen. Azure AD still thinks I'm logged in, I'm just not getting the tokens set properly in my application.
Since the authenticate flow is a little complex, I am trying illustrate it using a figure below:
First the redirect URL need to register to the identity provider, so the server will match the RedirectURL in request to ensure that the response is redirected as expected instead of redirect to like Phishing site( security consideration).
And as the figure demonstrate, to use the Azure AD as the external identity provider for IdentityServer3, we need to register the apps on Azure AD. However since the app is used to communicate with Identity Server, the redirect URL register on the Azure Portal should redirect to the IdentityServer3 instead of URL of app.
For example, the URL of my identity server 3 is https://localhost:44333 then I use code below to add the additional identity providers. And this URL is the redirect URL on the Azure portal:
public static void ConfigureAdditionalIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "aad",
Caption = "Azure AD",
SignInAsAuthenticationType = signInAsType,
Authority = "https://login.microsoftonline.com/04e14a2c-0e9b-42f8-8b22-3c4a2f1d8800",
ClientId = "eca61fd9-f491-4f03-a622-90837bbc1711",
RedirectUri = "https://localhost:44333/core/aadcb",
});
}
And the URL of my app is http://localhost:1409/ which is register on the IdentyServer3 and below code is the web app use OWIN OpenId Connect to add the IdentyServer3 as the identity data provider:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "cookies",
Authority = "https://localhost:44333",
ClientId = "mvc",
RedirectUri = "http://localhost:1409/",
ResponseType = "id_token",
Scope = "openid profile email"
});
After i finish my login code
var identity = new ClaimsIdentity(claims, OAuthConfigur.AuthenticationType);
this.AuthenticationManager.SignIn(new AuthenticationProperties
{
ExpiresUtc = DateTimeOffset.Now.AddMinutes(30),
IsPersistent = false
}, identity);
return RedirectToAction("Index", "Home");
After RedirectToAction , there is the cookie in broswer.
But when Authorize attribute there is no Authorize.
In my custom Authorize actionfilter ,
httpContext.User.Identity.IsAuthenticated
always return false.
I find a way to get identity below:
private ClaimsIdentity GetIdentity(HttpContextBase httpContext)
{
var ticket = httpContext.GetOwinContext().Authentication
.AuthenticateAsync(OAuthConfigur.AuthenticationType).Result;
var identity = ticket != null ? ticket.Identity : null;
return identity;
}
after this function, i can get the useridenttity.
Is this correct??
If i need users login info , i need call this function everytime is action?
Thank you reply!
Here's my Startup.cs
public void ConfigureAuth(IAppBuilder app)
{
// Enable Application Sign In Cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = OAuthConfigur.AuthenticationType,
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString(OAuthPaths.LoginPath),
LogoutPath = new PathString(OAuthPaths.LogoutPath),
ExpireTimeSpan = TimeSpan.FromMinutes(20)
});
// Setup Authorization Server
app.UseOAuthAuthorizationServer(new CustomerOAuthAuthorizationServerOptions());
}
Just in case someone stumbles upon this in the future. I had the same issue and I was pulling my hair out when I realised that I had set the
CookieSecure = CookieSecureOption.Always
on the CookieAuthenticationOptions class :/
So obviously cookies were only access over https and because my local environment was not setup with https (It used to be) it could not read the cookie.
I have one scenario when published the application to Production server the call httpContext.GetOwinContext().Authentication
.AuthenticateAsync("Application") always return null in IE browser. For this case, go to IE browser Internet Options -> Trusted sites, add your identity server application url as trusted site. System works then.