Adding location header on response from Microsoft.Owin.Security.OpenIdConnect middleware - c#

We have implemented OAuth using IndentityServer and Owin self hosted web API's. Whenever a client tries to access our authenticated endpoints we use Microsoft.Owin.Security.OpenIdConnect middleware to intercept and redirect the call to the IndentityServer for authentication. In the case of an API call we do not want to return 302, but instead a 401 with a location header with the url of the IdentityServer. We can get the OWIN middleware to return a 401 by setting
AuthenticationMode = AuthenticationMode.Passive
but we're unable to add a location header. How do we accomplish this? We have tried setting the header (see code below), but it doesn't work. It seems the response is build internally by the middleware.
appBuilder.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = idSrvUri.ToString(),
AuthenticationType = WebServiceConstants.OAuthAuthType,
ClientId = "beocloud",
Scope = "openid profile roles",
ResponseType = "id_token token",
AuthenticationMode = AuthenticationMode.Passive,
SignInAsAuthenticationType = WebServiceConstants.OAuthAuthType,
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest)
{
n.ProtocolMessage.RedirectUri = n.Request.Scheme + "://" + n.Request.Host + "/";
n.Response.Headers.Add("Location", new []{n.ProtocolMessage.CreateAuthenticationRequestUrl()});
}
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
}

This is the problem hosting a Web API along side your MVC code -- you're using the wrong security middleware for the API side of it. The OIDC middleware assumes the HTTP calls are from a browser window that it can redirect.
I'd suggest splitting your API into a separate host and pipeline and use a token-based security architecture and middleware. We have lots of samples of this pattern on the github repo.

Related

How to add OpenID combined with Forms Authentication to MVC

I have an existing MVC project that uses FormsAuthentication for its authentication.
I need to incorporate the option of logging in with an OpenID IDP in addition to the regular login page already available.
The problem I'm having is challenging the IDP on demand and setting the authentication cookie once the claims are received, I can't find the reason why the cookie is not sticking. The flow seems to be working fine, and I can see the claims in the AuthorizationCodeReceived callback.
Here's the Startup.Auth.cs code:
var notificationHandlers = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (context) =>
{
string username = context.AuthenticationTicket.Identity.FindFirst("preferred_username").Value;
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(60), true, "");
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
if (context.OwinContext.Request.Path.Value != "/Account/SignInWithOpenId")
{
context.OwinContext.Response.Redirect("/Account/Login");
context.HandleResponse();
}
return Task.FromResult(0);
}
};
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "Cookies",
Authority = "xxxxxxxxx",
ClientId = "MyClient",
ClientSecret = "xxxxxxxx",
RedirectUri = "http://localhost:52389/",
PostLogoutRedirectUri = "http://localhost:52389/",
ResponseType = "code id_token",
Scope = "openid profile email roles",
UseTokenLifetime = false,
TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = "preferred_username",
RoleClaimType = "role"
},
Notifications = notificationHandlers
});
app.SetDefaultSignInAsAuthenticationType("Cookies");
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies",
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider()
});
app.UseStageMarker(PipelineStage.Authenticate);
And here's the AccountController SignInWithOpenId method:
public ActionResult SignInWithOpenId()
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType);
// If I don't have this line, reponse redirects to the forms authentication login... so maybe something is wrong here?
return new HttpUnauthorizedResult("IDP");
}
else
{
return RedirectToAction("Index", "Default");
}
}
Any pointers would be greatly appreciated. Thank you.
This is the exact thing I'm trying to do at the moment. I will let you know if I find anything useful.
Update:
I ended up disabling Forms Authentication in the MVC web app. I was doing a proof of concept so it wasn't a hard requirement. I know this was not really what you were getting at. I successfully used my IdP to login and redirect back to the web app. Where the proof of concept ended was the HttpContext.User object was needed to be populated.
I was able to get this, or at least an equivalent, working in .NET 4.7. My use case is that most subscribers are being upgraded to log in via Azure AD B2C, but we have public PCs that we want to authenticate with a manual claim via an obscured URL.
I'm using Microsoft.Owin.Security.OpenIDConnect and related packages, and the Owin startup is standard, although I will point out this line:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
I had to disable Forms authentication entirely; I could not get this working when anything other than Anonymous Authentication was enabled in IIS.
The core of the solution was actually an example I found here: How to use OWIN forms authentication without aspnet identity
/* URL validated, add authenticated claim */
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "PublicPC"),
new Claim(ClaimTypes.Email, "PublicPC#example.org")
};
var id = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
var ctx = HttpContext.Current.GetOwinContext();
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(id);
But critically, I needed to specify CookieAuthenticationDefaults.AuthenticationType, which is what I'm using in the Owin startup.
solved by adding these code to Global.asax:
protected void Application_BeginRequest()
{
Context.Response.SuppressFormsAuthenticationRedirect = true;
}
according to Prevent ASP.NET from redirecting to login.aspx

CORS issue with UseOpenIdConnectAuthentication

Here Angularjs is front end and Web API is middle tier. we are using AzureAD OpenID connect for Authentication.
I'm facing following issue. because of the my landing page is not loading
Access to XMLHttpRequest at 'https://login.microsoftonline.com/xx-86f1-41af-91ab-xxx/oauth2/authorize?client_id=xxxx1&response_mode=form_post&response_type=code%20id_token&scope=openid%20profile&state=OpenIdConnect.AuthenticationPropertiesxxxxx&noncexxxxx&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2F&x-client-SKU=ID_NET451&x-client-ver=5.2.1.0' (redirected from 'https%3A%2F%2Flocalhost%3A44300%2F/api/Scorecard/GetServiceNameWithManagers?loginUser=xxx#microsoft.com') from origin 'https%3A%2F%2Flocalhost%3A44300%2F' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I've done lot of research and applied Access-Control-Allow-Origin =* at Request and response. also applied app.UseCors(Owin.Cors.CorsOptions.AllowAll);
but so far no success.
consider following code, AuthorizationCodeReceived delegate is not invoking very first time even though the user is logged in to microsoft site.
Please be noted, this code is not working very first time. It will work after few button clicks (postbacks) and then after few minutes if we run the application it's throws CORS preflight issue. Please help.
This is my startup.cs
public void Configuration(IAppBuilder app)
{
app.UseCors(Owin.Cors.CorsOptions.AllowAll);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new SystemWebChunkingCookieManager(),
});
//// Bearer token authentication middleware.(Ex: request from web clients,ajax calls able to pass authenticated bearer info)
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
TokenReplayCache = new TokenReplayCache(new MemoryCacheProvider())
},
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
Provider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = ctx =>
{
//// Retrieve user roles from the request.
var authenticationTicket = ctx.Ticket;
if (authenticationTicket.Identity.IsAuthenticated)
{
////Use the block when role/user specific authorization needs and to modify the user identity claims based on requirement
}
return Task.FromResult(0);
},
OnRequestToken = ctx => { return Task.FromResult(0); }
}
});
//// Non Bearer authentication middleware. (Ex: request secured web api call directly from URL/Web API server scope it self)
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
ClientSecret = ConfigurationManager.AppSettings["ida:AppKey"],
Authority = Authority,
PostLogoutRedirectUri = PostLogoutRedirectUri,
AuthenticationMode = AuthenticationMode.Active,
ResponseType = "code id_token",
CallbackPath = new PathString("/"),
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = context =>
{
if (context.AuthenticationTicket.Identity.IsAuthenticated)
{
////Use the block when role/user specific authorization needs and to modify the user identity claims based on requirement
}
return Task.FromResult(0);
},
AuthorizationCodeReceived = async context =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(ClientId, Models.ConfigurationData.GraphSecret);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, credential, GraphResource);
},
RedirectToIdentityProvider = context =>
{
if (context.ProtocolMessage.RedirectUri == null)
{
////To set the reply/redirect Url based on the request host environment.
////Hosting env details we get only through the owin context in startup and this is the delegate to set reply URL in OWincontext before the authentication.
string ReplyAddress = context.Request.Scheme + "://" + context.Request.Host + "/";
context.ProtocolMessage.RedirectUri = ReplyAddress;
}
//context.OwinContext.Authentication.User.Identity.IsAuthenticated = true;
if (context.OwinContext.Authentication.User.Identity.IsAuthenticated && context.ProtocolMessage.RequestType != IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout)
{
////To avoid infinite loop of redirections in request if user is authenticated and unauthorized.
context.HandleResponse();
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
return Task.FromResult(0);
}
},
TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "roles",
TokenReplayCache = new TokenReplayCache(new MemoryCacheProvider())
},
});
System.Web.Helpers.AntiForgeryConfig.UniqueClaimTypeIdentifier = System.IdentityModel.Claims.ClaimTypes.NameIdentifier;
}
Access to XMLHttpRequest at 'https://login.microsoftonline.com/xx-86f1-41af-91ab-xxx/oauth2/authorize?client_id=xxxx1&response_mode=form_post&response_type=code%20id_token&scope=openid%20profile&state=OpenIdConnect.AuthenticationPropertiesxxxxx&noncexxxxx&redirect_uri=https%3A%2F%2Flocalhost%3A44300%2F&x-client-SKU=ID_NET451&x-client-ver=5.2.1.0' (redirected from 'https%3A%2F%2Flocalhost%3A44300%2F/api/Scorecard/GetServiceNamxxxManagers?loginUser=xxx#microsoft.com') from origin 'https%3A%2F%2Flocalhost%3A44300%2F' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I think you misunderstood the error message. It says that your AngularJS application was trying to make a request to https://login.microsoftonline.com/xxx-xx-41af-91ab-xxx/oauth2/authorize and failed, because it was a cross-origin request and the server didn't approve it by returning the Access-Control-Allow-Origin header in a response to preflight request (HTTP method OPTIONS).
So you cannot change it by adding CORS headers to your backend. The authorize is not designed to be called by XMLHttpRequest requests - you are supposed to make a full browser request to that URL. Later, the browser will be redirected to redirect_uri (the request parameter value) along with an auth code or an error.

OpenId Connect and Custom Identity Framework

I'm using the Okta example for implementing OpenIdConnect in an Asp.NET 4.6.x MVC web application. The application uses Unity for Dependency Injection and one of the dependencies is a custom set of classes for the Identity Framework. I'm not using the Okta API because the IdP is not actually Okta and I'm assuming there's proprietary stuff in it. So it's all .NET standard libraries for the OpenId portions.
I can walk through the code after clicking login and it will carry me to the IdP and I can log in with my account, and then it will bring me back and I can see all of the information from them for my login. But it doesn't log me in or anything as it does in the example from Okta's GitHub.
Basically I'm wondering if the identity customization is what's interfering with the login and if there's a way to get in the middle of that and specify what I need it to do?
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions {
ClientId = clientId
, ClientSecret = clientSecret
, Authority = authority
, RedirectUri = redirectUri
, AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive
, ResponseType = OpenIdConnectResponseType.CodeIdToken
, Scope = OpenIdConnectScope.OpenIdProfile
, PostLogoutRedirectUri = postLogoutRedirectUri
, TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" }
, Notifications = new OpenIdConnectAuthenticationNotifications {
AuthorizationCodeReceived = async n =>
{
//var tokenClient = new TokenClient($"{authority}/oauth2/v1/token", clientId, clientSecret);
var tokenClient = new TokenClient($"{authority}/connect/token", clientId, clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
//var userInfoClient = new UserInfoClient($"{authority}/oauth2/v1/userinfo");
var userInfoClient = new UserInfoClient($"{authority}/connect/userinfo");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var claims = new List<System.Security.Claims.Claim>();
claims.AddRange(userInfoResponse.Claims);
claims.Add(new System.Security.Claims.Claim("id_token", tokenResponse.IdentityToken));
claims.Add(new System.Security.Claims.Claim("access_token", tokenResponse.AccessToken));
if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
{
claims.Add(new System.Security.Claims.Claim("refresh_token", tokenResponse.RefreshToken));
}
n.AuthenticationTicket.Identity.AddClaims(claims);
return;
}
, RedirectToIdentityProvider = n =>
{
// If signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenClaim = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenClaim != null)
{
n.ProtocolMessage.IdTokenHint = idTokenClaim.Value;
}
}
return Task.CompletedTask;
}
}
});
The token(s) returned by Okta have to be managed by your application in order to perform the login action. The OIDC token returned will need to be verified and validated by you, and then a decision made as to whether to accept the OIDC token. If so, you take action to log the user into your application. Recieving an OIDC token as a result of an OpenID Connect flow doesn't by itself log you into an app. The app needs to do some more work based on the token content before taking a login or reject action.

Unable to obtain username from WebAPI in ADFS4-OAuth2 OpenID

I am trying to use ADFS Authentication with OAuth to communicate between my webapp and webapi. I am using ADFS4 and have configured application group with Server application and Webapi accordingly. I am trying to receive the userdetails, particularly the username from the webapi controller. Is it possible to pass the username details within the access token passed to webapi. Here is what I did from the Webapp side:
In the webapp controller after adfs authentication,
authContext = new AuthenticationContext(Startup.authority, false);
ClientCredential credential = new ClientCredential(Startup.clientId, Startup.appKey);
string accessToken = null;
bool isAuthenticated = User.Identity.IsAuthenticated; //return true
string username = User.Identity.Name; // returns username
string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Name).Value; // returns username
HttpClient httpClient = new HttpClient();
try
{
result = authContext.AcquireTokenAsync(Startup.apiResourceId, credential).Result;
accessToken = result.AccessToken;
}
catch (AdalException ex)
{
}
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
HttpResponseMessage response = httpClient.GetAsync(Startup.apiResourceId + "/api/ConfApi").Result;
From the Webapi end, in Startup.Auth.cs, I have added these code
public void ConfigureAuth(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseActiveDirectoryFederationServicesBearerAuthentication(
new ActiveDirectoryFederationServicesBearerAuthenticationOptions
{
MetadataEndpoint = ConfigurationManager.AppSettings["ida:AdfsMetadataEndpoint"],
TokenValidationParameters = new TokenValidationParameters() {
SaveSigninToken = true,
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
}
});
}
However, within the ConfApi controller, I cannot find any claims with user details.
What can I do to receive user details in the Webapi controller?
Thanks for any help.
Are you actually receiving the claims?
Did you configure claims rules for the web API on the ADFS side?
What did you use for Name - Given-Name, Display-Name etc?
Use something like Fiddler to monitor the traffic. After the OIDC authentication, you should see access tokens, id tokens etc.
Take the token and copy into jwt.io.
This will show you what you are actually receiving.
However, the OWIN classes translate the simple OAuth attributes e.g. "aud" into the claim type URI e.g. http://claims/this-claim so breakpoint and see what is in the claims collection and what type has been assigned to each.
The answer to this is the same answer to the question: MSIS9649: Received invalid OAuth request. The 'assertion' parameter value is not a valid access token
You have to use authorization code flow (instead of client credentials grant flow) to get the server app (web app in this case) to talk to the web API with the user's context. Authorization code flow will pass the claims in the JWT Token. Just make sure you pass thru any claims you need for the web API in the web API's RPT claim issuance transform rules.
Vittorio has a nice post on authorization code flow, although it talks about azure.
In order to use authorization code flow, you need to handle the AuthorizationCodeReceived Event via Notifications on the OpenIdConnectAuthenticationOptions from Startup.ConfigureAuth(IAppBuilder app)
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
...
Notifications = new OpenIdConnectAuthenticationNotifications {
AuthorizationCodeReceived = async code => {
ClientCredential credential = new ClientCredential(Startup.clientId, Startup.appKey);
AuthenticationContext authContext = new AuthenticationContext(Startup.authority, false);
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
code.Code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
Startup.apiResourceId);
}
}
When you are ready to make the call you acquire your token silently.
var authContext = new AuthenticationContext(Startup.authority, false);
var credential = new ClientCredential(Startup.clientId, Startup.appKey);
var claim = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
var userId = new UserIdentifier(claim, UserIdentifierType.UniqueId);
result = await authContext.AcquireTokenSilentAsync(
Startup.apiResourceId,
credential,
userId);
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
result.AccessToken);

HttpClient to call Azure AD-protected site

Following some Microsoft samples, I got to this point:
ASP.NET Core setup:
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAD:ClientId"],
Authority = Configuration["Authentication:AzureAd:Authority"],
ResponseType = OpenIdConnectResponseType.IdToken,
AutomaticAuthenticate = true,
TokenValidationParameters = new TokenValidationParameters()
});
AuthorizationTest endpoint:
[HttpGet]
[Authorize]
public IActionResult Get()
{
return Ok("SAMPLE TEXT - if you can read this then call it a day :)");
}
Client:
try
{
var result = await authContext.AcquireTokenAsync(WebApiResourceId, WebApiClientId, WebApiRedirectUri, new PlatformParameters(PromptBehavior.Auto));
authorizedClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
var authorizedMessage = await authorizedClient.GetAsync("/AuthorizationTest");
var statusCode = authorizedMessage.StatusCode.ToString();
var message = await authorizedMessage.Content.ReadAsStringAsync();
webBrowser.NavigateToString(message);
}
And the authorizedClient is initiated as:
private static HttpClientHandler handler = new HttpClientHandler
{
AllowAutoRedirect = true,
CookieContainer = new CookieContainer(),
UseCookies = true
};
private static HttpClient authorizedClient = new HttpClient(handler, false) { BaseAddress = WebApiBaseUri };
I used to initialize it only with the BaseAddress, and later added the handler following an answer here on So.
The problem:
Even though I get the token from AAD correctly, the response from the WEB API endpoint is an HTML (after an auto-redirect) that is the MS login page with the error "Your browser is set to block cookies....."
What should I change to make the HttpClient work? Or can I change the WebApi configuration to not use cookies? For the latter option I couldn't find any other alternative.
As discussed in the comments, you need to use the JWT bearer token middleware from the package Microsoft.AspNetCore.Authentication.JwtBearer.
The Open ID Connect middleware is designed to redirect a user to a sign in page, not for authenticating access tokens. An example usage of the JWT bearer token middleware can be found here: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore/blob/master/TodoListService/Startup.cs.
Take a look at this thread: https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/514 - it is showing the scenario you are trying to achieve.

Categories

Resources