AuthorizationCodeReceived event not firing - asp.net-mvc

I'm attempting to implement the OpenId Connect middleware in a an ASP.NET MVC 5 (.Net Framework) application.
In my AccountController.cs I send an OpenID Connect sing-in request. I have another OpenId connect middleware implemented which is why I specify that the middleware I want to challenge against is "AlternateIdentityProvider".
public void SignIn()
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
"AlternateIdentityProvider");
}
Upon issuing a challenge against the middleware, the RedirectToIdentityProvider event in Startup.cs fires and I am redirected to the provider for sign in. However, after successfully signing in I am redirected to the specified redirect uri with the state and code parameters added as query parameters i.e. http://localhost:63242/singin-oidc/?state=State&code=AuthorizationCode (parameters removed for brevity), which results in a 404 as no such route exists in my application.
Instead I expected the successful signin to trigger the AuthorizationCodeReceived event where I can implement my additional logic. In fact none of the other events ever trigger.
I have implemented an almost identical solution in ASP.Net Core 2.1 and here I am able to step through the different events as they trigger.
The relevant code of my current Startup.cs is shown below. Note that the OpenId provider throws an error if the inital request include reponse_mode and some telemetry parameters, hence these are removed during the initial RedirectToIdentityProvider event.
Any ideas why the callback from the OpenId provider is not getting picked up in the middleware?
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions("AlternateIdentityProvider")
{
ClientId = { { Client Id } },
ClientSecret = { { Client Secret } },
Scope = OpenIdConnectScope.OpenId,
ResponseType = OpenIdConnectResponseType.Code,
RedirectUri = "http://localhost:63242/singin-oidc",
MetadataAddress = { { Discovery document url } },
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = context =>
{
Debug.WriteLine("Redirecting to identity provider for sign in..");
context.ProtocolMessage.EnableTelemetryParameters = false;
context.ProtocolMessage.ResponseMode = null;
return Task.FromResult(0);
},
AuthorizationCodeReceived = context => {
Debug.WriteLine("Authorization code received..");
return Task.FromResult(0);
},
SecurityTokenReceived = context =>
{
Debug.WriteLine("Token response received..");
return Task.FromResult(0);
},
SecurityTokenValidated = context =>
{
Debug.WriteLine("Token validated..");
return Task.FromResult(0);
},
}
});

I was encountering the same issue. I am trying to plug in Owin into our legacy WebForms app.
For me, I had to do the following:
1) Change the application manifest of the application definition on Azure to set the "oauth2AllowIdTokenImplicitFlow" property to true from false.
Go to the Azure Portal,
Select to Azure Active Directory
Select App Registrations
Select your app.
Click on Manifest
Find the value oauth2AllowIdTokenImplicitFlow and change it's value to true
Click Save
2) In your startup.cs file, change the following:
ResponseType = OpenIdConnectResponseType.Code
to
ResponseType = OpenIdConnectResponseType.CodeIdToken
Once, I did those two things, the SecurityTokenValidated and AuthorizationCodeReceived started firing.
Though, I am not sure this is the right way to go or not. Need to do more reading.
Hope this helps.

Please be aware that the OpenId Connect implementation in .Net Framework only support response_mode=form_post. (See closed GitHub issue)
Since you strip the parameter in the request to the OpenId Connect provider (in your RedirectToIdentityProvider notification), then the provider will default to response_mode=query pr. the specs. (see relation between response_type and response_mode ind the specs.)
So in short the OpenId Connect middleware expects that there will come a HTTP POST (with a form body) and your provider will properly send a HTTP GET (with parameters as query-string).

Related

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

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

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

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

Use POSTMAN to authenticate to OPENID server

I'm new to OpenID and OAuth 2.0.
I have an API of my own (it´s not in .net core but in .net 4.6) and I'm trying to use Postman by sending a request with an access token to access a [Authorize] resource in a API application of my own.
The api is configured to invoke the OpenID server in Implicit Mode, so I can get a access_token and use it from another client.
I don't have any problems when I debug my application; it authenticates against the OAUTH server and saves the state, but for some reason, when I try to send a request to the resource of my API, it still redirects me to the Authentication page of the OAUTH server as if I wasn't logged in.
This is the Startup in my API:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieName = "AuthCookieCoolApp",
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = OidcAuthority,
ClientId = OidcClientId,
//ClientSecret = OidcClientSecret,
GetClaimsFromUserInfoEndpoint = true,
PostLogoutRedirectUri = OidcRedirectUrl,
RedirectUri = OidcRedirectUrl,
// ResponseType = OpenIdConnectResponseType.Code,
Scope = OpenIdConnectScope.OpenId,
RequireHttpsMetadata = false,
ResponseType = OpenIdConnectResponseType.CodeIdTokenToken,
Notifications = new OpenIdConnectAuthenticationNotifications
{
MessageReceived = notification =>
{
var message = notification.ProtocolMessage;
var accesstoken = message.AccessToken;
return Task.FromResult(0);
}
}
});
}
Maybe it has something to do with the cookies, but I'm not sure.
Thing is, This works fine when I debug my asp.net app: the endpoints marked with the [Authorize] attribute are only allowed when authenticated in the OAuth Server. But for some reason, I cannot use this from Postman:
First, I send this request to get an access_token:
Then I try to use that same obtained Access Token in a different request invoking the [Autorize] method of my API, but it redirects me to the login page of the OAuth server:
I'm probably setting something wrong in the API, not sure what.
Has anyone experienced something like this?
Header authorize looks suspicious. Usually Authorization header is used for authentication. But you may have custom implementation, where authorize header is valid.

OWIN WsFederation authentication with in-app authorization

The scenario:
A ASP.NET MVC Web app for a company's internal users, configured for authentication against the company's ADFS, using Microsoft.Owin.Security.WsFederation
The company's ADFS contains several users, but only a few of them should be able to log in to the application.
I therefore have a database table containing these users' email addresses
The web app should check if the email claim received from ADFS exists in the DB table, and only issue a log in token for those users.
Other users should be redirected to a "Sorry you are not authorized to use this application"-page.
My question:
Where is the correct place to put the authorization logic that checks if an user should be allowed in?
Here's the code in my Startup.Configuration method:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
MetadataAddress = "https://.../FederationMetadata.xml",
Wtrealm = "...",
}
);
You have two options to achieve what you want:
1. One is to configure this at the very AD FS. I personally think this is the right way to do this, since the AD FS is the IdP and it should be the one controlling whether or not its users have access to the application. In this case the company should or should not allow somebody to use some of its resources (of course there are anti-arguments). This can be easily done at the Domain Controller, through the AD FS Management GUI. The following answer greatly describes this:
https://serverfault.com/a/676930/321380
2. Second is to use the Notifications object at the OWIN WSFed middleware in this way:
Notifications = new WsFederationAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
//extract claims' values and check identity data against your own authorization logic
bool isAuthorized = CheckForUnauthorizedAccess();
if (!isAuthorized)
{
throw new SecurityTokenValidationException("Unauthorized access attemp by {some_identifier}");
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
if (context.Exception is an unauthorized exception)
{
context.OwinContext.Response.Redirect("<unauthorized_redirect_url>");
}
context.HandleResponse(); // Suppress the exception
//exception logging goes here
return Task.FromResult(0);
}
}

Enabling SSL in ASP.NET MVC 5 app results in OpenIdConnectProtocolValidator issue

I have an ASP.NET MVC 5 app that authenticates against Azure Active Directory. I wanted to enable SSL on it across the app. and hence leveraged global filters as follows:
public class FilterConfig
{
/// <summary>
/// Registers the global filters.
/// </summary>
/// <param name="filters">The filters.</param>
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new RequireHttpsAttribute());
}
}
After this I also set 'Enable SSL' in the project's properties to true. This gave me the following SSL URL -> https://localhost:34567. I updated the project to have this in its IIS Express path under the 'Web Tab' under Servers in 'Project URL'. However on running the site I run in to the following error:
IDX10311: RequireNonce is 'true' (default) but validationContext.Nonce is null. A nonce cannot be validated. If you don't need to check the nonce, set OpenIdConnectProtocolValidator.RequireNonce to 'false'.
I have auth. enabled on the site. I use Azure Active directory.
The security code is as follows:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri
});
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = audience,
Tenant = tenant,
});
The auth. values are being read from the web.config and are as follows:
<add key="ida:ClientId" value="<some_guid>" />
<add key="ida:Audience" value="https://localhost:34567/" />
<add key="ida:AADInstance" value="https://login.windows.net/{0}" />
<add key="ida:Tenant" value="microsoft.onmicrosoft.com" />
<add key="ida:PostLogoutRedirectUri" value="https://localhost:34567/" />
I tried setting RequireNonce to false as directed in the error message as follows:
ProtocolValidator = new OpenIdConnectProtocolValidator
{
RequireNonce = false
}
But this just resulted in an invalid request error.
Could someone help me understand what the problem is here? Everything worked great until SSL was enabled.
You can ignore exceptions if the error message starts with OICE_20004 or contains IDX10311. Note: do it on your own risk.
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
// Ensure the URI is picked up dynamically from the request;
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + context.Request.Uri.PathAndQuery;
context.ProtocolMessage.RedirectUri = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + context.Request.Uri.PathAndQuery;
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
if (context.Exception.Message.StartsWith("OICE_20004") || context.Exception.Message.Contains("IDX10311"))
{
context.SkipToNextMiddleware();
return Task.FromResult(0);
}
return Task.FromResult(0);
},
}
From the Azure management portal, check that your application under the corresponding active directory has the same Sign On URL and reply URL.
If they are not same, you will get this error.
This happens when you enable SSL because it changes only the sign on URL to the HTTPS URL while the reply URL remains the same HTTP URL.
Edit:
Read on if you want to know exactly why this is happening,
When you try to access your app using the https URL, it sets a cookie with a unique number(nonce) in your browser and hits Azure AD for authentication. After authentication, the browser has to give access to that cookie. But since the sign on URL and reply URL are different the browser does not recognise your app and does not give access to that cookie and hence the application throws this error.
I can reproduce this error by pressing back button couple of times on my web application, even after successful login.
can you try these 2 things:
in your code below:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = mViewWebSite.ClientId,
Authority = mViewWebSite.Authority,
PostLogoutRedirectUri = mViewWebSite.PostLogoutRedirectUri
});
add protocol validator as on of the authentication options, as what error suggest:
ProtocolValidator = new Microsoft.IdentityModel.Protocols.OpenIdConnectProtocolValidator(){
RequireNonce = false
}
or add notification, by this you can catch this error and redirect it to some error page. I do that to make it graceful. Until Katana people fixes it.
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error.aspx?message=" + context.Exception.Message);
return Task.FromResult(0);
}
},
I manage to work around this problem using following method in the Global.asax file. At least this won't show the exception to the client. I use ELMAH to catch exceptions.
protected void Application_Error(object sender, EventArgs args)
{
var ex = Server.GetLastError();
if (ex.Message.Contains("IDX10311:"))
{
Server.ClearError();
Response.Redirect("https://www.yoursitename.com");
}
Well it would probably be best to look at the katana source code, from that i found the exception type to be OpenIdConnectProtocolInvalidNonceException so i handle it like this.
if (n.Exception is OpenIdConnectProtocolInvalidNonceException &&
n.OwinContext.Authentication.User.Identity.IsAuthenticated)
{
n.SkipToNextMiddleware();
return;
}
I have this exception popup on browsers that cache the pages and users that click the back button after login.
The issue here is simple... took me hours to figure this out.
Since I was testing on my local had no https and to tell you the truth when initially creating my app in Azure AD since i wasnt expecting it to be https during my test I made it plain http (replyUrl's HomePage Url, Logout all that jazz)
Then after doing this i encountered the infinate loop issue a lot of people are getting. so then i decided to mock the cert on my local and yep that got rid of the infinate redirect but then brought another one the "IDX10311: RequireNonce is 'true' " one
Long story short... make your AzureAD App https in all its endpoints. and wallah!
#zb3b answer + #jonmeyer answer:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
...
Notifications = new OpenIdConnectAuthenticationNotifications()
{
...
AuthenticationFailed = (context) =>
{
if ((context.Exception is OpenIdConnectProtocolInvalidNonceException) &&
(context.OwinContext.Authentication.User.Identity.IsAuthenticated))
{
context.SkipToNextMiddleware();
return Task.FromResult(0);
}
return Task.FromResult(0);
},
...
}
});
Just adding another case I just ran into: the network you connect to may be modifying HTML content.
A customer called with an issue: he could not get past this error. It was a new laptop where he had not logged on before. After about one hour of trying several possible solutions, I decided to check the network he was connected to.
It turns out he was connected to a network in an airport, open and unsecured, and not using a VPN service (lacking some SETA there). I don't know exactly who operated that network or what they were doing, but the Azure AD service must have detected some type of tampering with the nonce.
The moment the user connected to a trusted network, the issue was resolved.

Resources