IdentityServer3 + AzureAD and RedirectUri Confusion - c#

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"
});

Related

Need help adjusting ASP.NET that uses Azure AD to also allow SSO from third party

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.

How to switch Azure AD directory programatically via C# in a multi tenant app

I have ASP.NET MVC app which is configured with a Multi-tenant Azure AD app. So in my Web.config I had to put <add key="ida:Authority" value="https://login.microsoftonline.com/common/" /> so this will work for any one. But now some of my apps users are assigned to different Azure Directories (where Tenant IDs differ). I need to provided a "Switch Directory" functionality like in Azure Portal / Aure IoT Central.
I know if I replace common with the Tenant ID, the app will only work for the particular Tenant.
In the public void ConfigureAuth(IAppBuilder app) of Startup.Auth.cs I have something like below
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions { });
app.UseCookieAuthentication(new CookieAuthenticationOptions ());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
ClientSecret = clientSecret,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = redirectUri,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = (context) =>
{
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
// The above tenantID is always coming from the default directory
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
// The above is also always from the default directory (tenant id)
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
return Task.FromResult(0);
},
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
HttpContext.Current.Session["CODE"] = code;
return Task.FromResult(0);
},
AuthenticationFailed = context =>
{
return Task.FromResult(0);
}
}
});
}
So this gives a OWIN context to me which has the details only for the default directory ID. Can anyone help me to change this behavior? so that I can provide a feature to switch between directories.
A hack I made once (with OWIN) was to add a specific query parameter when requesting login that specified a tenant id.
I can't find the source code right now, but an OWIN middleware did the following:
Call next middleware
Is the response a GET, the response statuscode a 302, location header value has the Azure AD host, and the tenant id query parameter is present?
If yes, grab the Location header, switch the value to "https://login.microsoftonline.com/tenant-id/oauth2/authorize?" + locationHeader query string
It's a kind of hack, but works to help a user log in to the right tenant.

Implementing Sliding Expiration in OpenIdConnect with Azure

We have a ASP.Net MVC and have been using OpenIdConnect authentication with Azure AD as the authority. On successful authentication we set the "AuthenticationTicket" Expiry to 8hrs (below i have set to 15 minutes for testing). This 8hrs is fixed, meaning even if user performs activity on the application it won't slide.
But ideally we would want the expiration to slide as a user is active with the system.
Tried setting "SlidingExpiration" to True, even that didn't help. Documentation is not detailed around this topic.
So, How do we implement sliding expiration with OpenIdConnect Authentication?
Below is our code of startup.
namespace TestApp
{
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebCookieManager(),
SlidingExpiration = true,
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
//
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
//
AuthorizationCodeReceived = (context) =>
{
context.AuthenticationTicket.Properties.ExpiresUtc = DateTime.UtcNow.AddHours(8);
context.AuthenticationTicket.Properties.AllowRefresh = true;
context.AuthenticationTicket.Properties.IsPersistent = true;
return Task.FromResult(0);
},
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
},
}
});
}
}
}
Hopefully someone from the ASP.net or Owin team can jump in with a better way to do this, but below is how I got around the exact same problem.
In order for the cookie authentication to take precedence and return a 401 or redirect when the cookie is expired, you need to set the cookie authentication mode to active and the Open Id authentication mode to passive.
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(15),
CookieSecure = CookieSecureOption.Always,
LoginPath = Microsoft.Owin.PathString.FromUriComponent("/Logout")
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
PostLogoutRedirectUri = postLogoutRedirectUri,
}
);
After changing from active to passive, I was getting a 401 response when requesting an authorized resource, but refreshing the page would simply log me back in using the token saved from the OpenID call which is not expired when the token expires. To fix this, I set an action in the logout controller (specified with the LoginPath on the cookie options) to sign the user out of both OpenID and Cookie
[Route("Logout")]
public ActionResult Logout()
{
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
return View("~/Views/Index/Logout.cshtml");
}
Keep in mind, with Open ID set to passive and cookie set to active, all requests will be redirected as unauthorized if they don't contain the cookie (even if they contain a token). Open ID requests will have to specifically call the Open ID handler to be authorized and create the cookie
sources:
https://msdn.microsoft.com/en-us/library/microsoft.owin.security.cookies.cookieauthenticationoptions(v=vs.113).aspx
https://coding.abel.nu/2014/06/understanding-the-owin-external-authentication-pipeline/

OWIN Challenge() method not executed when using multiple ADFS + Cookies

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

ASP.NET Web API and OpenID Connect: how to get Access Token from Authorization Code

I try to get OpenID Connect running... A user of my Web API managed to get an Authorization Code of a OpenID Connect Provider. How am I supposed to pass this code to my ASP.NET Web API? How do I have to configure OWIN Middleware such that I can get an Access Token using the Authorization Code?
UPDATE:
A SPA uses AJAX for communicating with my web service (ASP.NET Web API). In my web service a use OWIN Middleware. I set OpenIDConnect as the authentication mechanism. When the web service is called for the first time it successfully redirected the user to the login page of the OpenID Connect Provider. The user could login and got an Authorization Code as a result. AFAIK this code could now be used (by my web service) to the an Access Token. However, I don't know how to get this code back to my web service (is this done using a header?) and then what to configure to get the Access Token. I guess I could call the token endpoint manually but I would like to take advantage of the OWIN component instead.
BenV already answered the question, but there's more to consider.
class partial Startup
{
public void ConfigureAuth(IAppBuilder app)
{
// ...
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
Notifications = new OpenIdConnectAuthenticationNotifications() {
AuthorizationCodeReceived = (context) => {
string authorizationCode = context.Code;
// (tricky) the authorizationCode is available here to use, but...
return Task.FromResult(0);
}
}
}
}
}
Two problems:
First of all, authorizationCode will get expired quickly. There's no sense in storing it.
The second problem is that AuthorizationCodeReceived event will not get fired for any of the page reloads as long as authorizationCode is not expired and stored inside the session.
What you need to do is to call AcquireTokenByAuthorizationCodeAsync which will cache it and handle properly inside TokenCache.DefaultShare:
AuthorizationCodeReceived = (context) => {
string authorizationCode = context.Code;
AuthenticationResult tokenResult = await context.AcquireTokenByAuthorizationCodeAsync(authorizationCode, new Uri(redirectUri), credential);
return Task.FromResult(0);
}
Now, before every call to the resource, invoke AcquireTokenSilentAsync to get the accessToken (it will use TokenCache or silently use refreshToken ). If token is expired, it will raise AdalSilentTokenAcquisitionException exception (invoke access code renew procedure).
// currentUser for ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")
AuthenticationResult authResult = await context.AcquireTokenSilentAsync(resourceUri, credential, currentUser);
Calling AcquireTokenSilentAsync is very fast if token is cached.
Looks like the recommended approach is to use the AuthorizationCodeReceived event to exchange the Auth code for an Access Token. Vittorio has a blog entry that outlines the overall flow.
Here's an example from this sample app on GitHub of the Startup.Auth.cs code to set this up:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}", tenantID), new EFADALTokenCache(signedInUserID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceID);
return Task.FromResult(0);
},
...
}
Note: The AuthorizationCodeReceived event is invoked only once when authorization really takes place. If the auth code is already generated and stored, this event is not invoked. You have to logout or clear cookies to force this event to take place.
This is now built into Microsoft.Owin 4.1.0 or later. You can use SaveTokens to make the id_token available and then RedeemCode to get an access token and make that available
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
ClientSecret = "redacted",
RedirectUri = postLogoutRedirectUri,
//This allows multitenant
//https://github.com/Azure-Samples/guidance-identity-management-for-multitenant-apps/blob/master/docs/03-authentication.md
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthenticationFailed = (context) =>
{
return Task.FromResult(0);
}
},
SaveTokens = true,
// Required for the authorization code flow to exchange for tokens automatically
// using this means you will need to provide RedirectUri and ClientSecret
RedeemCode = true
}
);
You can then access the tokens through the HttpContext object
var result = Request.GetOwinContext().Authentication.AuthenticateAsync("Cookies").GetAwaiter().GetResult();
string idToken = result.Properties.Dictionary["id_token"];
string accessToken = result.Properties.Dictionary["access_token"];
Sources:
How to get access token from httpcontext using owin and Mvc 5
You need to bypass the default owin validation to do custom Authorization:
new OpenIdConnectAuthenticationOptions
{
...,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false
},
TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RoleClaimType = ClaimTypes.Role },
This line of code solved my issue. We need to validate the issuer to be false.

Categories