Error using OpenID Connect with .NET Core 3.0 ASP.NET Core API service - asp.net-core

I'm trying to implement OpenID Connect with Azure AD in a .NET Core 3.0 ASP.NET Core API service.
It is an API service only, with no UI, and a separate SPA is used to access the API service.
Per the instructions here, I'm sending the sign-in request from my SPA, which, after authentication is being redirected back to the /signin-oidc endpoint on my API.
At this point, I get an error:-
Error from RemoteAuthentication: "Unable to unprotect the message.State.".
The initial request from the SPA looks like this:-
tenantId = my Azure AD tenant ID
clientId = my Azure AD application ID
responseType = "id_token"
redirectUri = "http://localhost:12345/signin-oidc"
scope = "openid"
responseMode = "form_post"
state = "12345"
nonce = "67890"
https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?client_id={clientId}&response_type={responseType}&redirect_uri={redirectUri}&scope={scope}&response_mode={responseMode}&state={state}&nonce={nonce}
And my API startup code looks like this:-
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureAD(o =>
{
o.ClientId = "(tenant id)";
o.TenantId = "(client id)";
o.Instance = "https://login.microsoftonline.com/";
})
.AddAzureADBearer(o =>
{
o.ClientId = "(tenant id)";
o.TenantId = "(client id)";
o.Instance = "https://login.microsoftonline.com/";
});
If I omit the state parameter from the initial request, I get a different error:-
Error from RemoteAuthentication: "OpenIdConnectAuthenticationHandler: message.State is null or empty.".
The referenced instructions say that state is recommended, not required:-
A value included in the request that also will be returned in the token response. It can be a string of any content you want. A randomly generated unique value typically is used to prevent cross-site request forgery attacks. The state also is used to encode information about the user's state in the app before the authentication request occurred, such as the page or view the user was on.
However, the errors I'm getting seem to imply that state is required, and needs to be specially generated.
I've also tried with the following startup code, and get the same errors:-
services.AddAuthentication(options =>
{
options.DefaultScheme = Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.ClientId = "(client id)";
options.Authority = "https://login.microsoftonline.com/" + "(tenant id)";
});

The problem I think is that you can not send requests directly to /signin-oidc from your SPA application. Because the AddOpenIdConnect(...) handler requires that it is the one who makes the initial request to AzureAd and it that it also who receives the request back to /signin-oidc.
AddOpenIdConnect sets its own state value and passes is to the Identity provider aand it expects to see the same state value later when the browser calls /signin-oidc.

Related

Can I create an Identity Server 4 ASP.NET Core API using 2 different token authentication middleware?

I am trying to figure out if its possible to write an ASP.NET Core API that consumes an identity server token using either Reference Tokens or JWT tokens based on whatever I've configured my identity server to use. The back-end configuration for IS4 is pretty easy, I'm just not convinced that I can configure 2 different token middlewares and my service will both be ok with it and know what to do.
So the idea is:
If my API gets a jwtToken, it attempts to use the jwt middleware for authorization back to identity server.
If my API gets a reference token, it attempts to use the introspection middleware for authorization back to identity server.
Obviously, if the wrong type of token is provided for whatever is configured on the IS4 service, it will fail.
Handling the token endpoint and revocation endpoint should also be easy enough, it's just the middleware magic I'm concerned with.
I know typically you wouldn't want to do this but we have a niche use case for it. All I'm currently concerned with is whether or not its even possible. I'm not familiar with how the auth middleware works in the back-end.
According to the Identity Server 4 Protecting APIs document, we can see that it supports to use both JWTs and reference tokens in asp.net core.
You can setup ASP.NET Core to dispatch to the right handler based on the incoming token, see this blog post for more information.
services.AddAuthentication("token")
// JWT tokens
.AddJwtBearer("token", options =>
{
options.Authority = Constants.Authority;
options.Audience = "resource1";
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
// if token does not contain a dot, it is a reference token
options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
})
// reference tokens
.AddOAuth2Introspection("introspection", options =>
{
options.Authority = Constants.Authority;
options.ClientId = "resource1";
options.ClientSecret = "secret";
});
Supporting both JWTs and reference tokens
In addition to #Zhi Lv post you might need to add Authorization policy, Authentication Schemes to allow validating JWT and reference tokens.
Here is the sample code template replace api name, api secret and audience appropriatly.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(Options =>
{
Options.Authority = "https://identity.domain.com/identity/";
Options.Audience = "resource1"; //your api baseurl e.g if you want userinfo_endpoint specify https://identity.domain.com/identity/connect/userinfo
Options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
})
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://identity.domain.com/identity/";
options.ApiName = "api name / scope";
options.ApiSecret = "api secret / scope secret";
});
services.AddAuthorization(options =>
{
options.AddPolicy("tokens", x =>
{
x.AddAuthenticationSchemes("jwt", "introspection");
x.RequireAuthenticatedUser();
});
});
}
The way I would do it is to use introspection and claims caching in both cases, so that the API does not need to know or care which type of access token it receives.
The introspection would only occur when an access token is first received. Subsequent requests with the same token then use cached claims.
RESOURCES
Blog Post
Sample C# Code

Configure an asp.net core Web App to validate JWT token from ADFS

I'm using ADFS 2019 and the scenario is:
Client App (trusted, client id and client secret)
Web Api (acts both as a server and as a client)
Resource to access
My GOAL is:
By using postman get a token from ADFS and call a Web API launched locally that must validate this token. Once the token has been validated it must generate another token (on-behalf-of) to access the last resource.
I can successfully get the first token specifying:
- Grant Type: Client Credentials
- Access Token URL: https://MY-ADFS/adfs/oauth2/token
- Client ID
- Client Secret
How can i configure my asp.net core Web Application to validate and accept this token?
I have all the data:
Web App identifier (for the server), web app client id/secret (when it acts as a client) and ADFS metadata endpoint.
I'm trying to do something like this:
services
.AddAuthentication(o =>
{
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://.../adfs";
options.Audience = "urn:microsoft:userinfo"; // taken from client token using jwt.io
options.MetadataAddress = "adfs metadata address";
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "https://.../adfs/services/trust",
ValidAudiences = new List<string> { "web app id" },
};
But it does not work (unauthorized or internal server error).
All these application are in the same application group in ADFS.
Thank you.
Update 1:
If i've understood correctly the audience must be WHO validates the token. So it must be the Web Api identifier inside ADFS.
If i put this identifier in the audience variable i get: audience did not match.
The audience that is in the token that i'm sending with postman is indeed different: urn:microsoft:userinfo!
Update 2:
I've managed to access to the web api and get a nice and valid access token. Now the problem is that the audience of the token is like:
"aud": "microsoft:identityserver:web api id on ADFS"
That "microsoft:identityserver is a problem when i have to do the "on-behalf of".
It forces me in doing:
ClientCredential clientCredential = new ClientCredential("microsoft:identityserver:client ID", "secret");
Otherwise it does not validate the audience.
But doing so, when i do:
var result = await authenticationContext.AcquireTokenAsync("resource to access' id", clientCredential, userAssertion);
It tells me that it cannot find a resource with client id "microsoft:identity:client id", and that's true, because the resource on ADFS has a client ID WITHOUT the "microsoft:identity" part.

Identity Server 4 Client Credentials for custom endpoint on token Server

I implemented a token server using Identity Server 4.
I added a custom API endpoint to the token server and struggle with the authentication. The custom endpoint is inherited from ControllerBase and has 3 methods (GET, POST, DELETE).
I intend to call the custom endpoint from within another API using a dedicated client with credentials (server to server) implemented as HttpClient in .NET Core. There is no user involved into this.
For getting the access token I use the IdentityModel DiscoveryClient and TokenEndpoint.
So in sum I did the following so far:
setup "regular" identity server and validate it works -> it works
implement custom endpoint and test it without authorizatio -> it works
add another api resource ("api.auth") with a custom scope "api.auth.endpoint1"
setup a client with client credentials allowing access to scope "api.auth.endpoint1".
implement the HttpClient and test setup -> I get an access token via the Identity Model Token Endpoint.
Now, when I call the endpoint using the HttpClient with the access token I received I get response code 200 (OK) but the content is the login page of the identity server.
The documentation of Identity Server 4 state the use of
services.AddAuthentication()
.AddIdentityServerAuthentication("token", isAuth =>
{
isAuth.Authority = "base_address_of_identityserver";
isAuth.ApiName = "name_of_api";
});
as well as the use of
[Authorize(AuthenticationSchemes = "token")]
Unfortunatly the compiler state that .AddIdentityServerAuthentication can't be found. Do I miss a special nuget?
The nugets I use on the token server so far are:
IdentityServer4 (v2.2.0)
IdentityServer4.AspNetIdentity (v2.1.0)
IdentityServer4.EntityFramework (v2.1.1)
Figured out that part. The missing nuget for AddIdentityServerAuthentication is:
IdentityServer4.AccessTokenValidation
Struggling with the authorization based on the custom scope.
Does anyone know how the security has to be configured?
Configure a client with ClientGrantTypes = client_credentials and your api like this:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth";
});
Where ApiName is the name of the resource. Please note that resource != scope. In most samples the resource name is equal to the scope name. But not in your case, where resource name is api.auth and scope name is api.auth.endpoint1.
Configure the client to request the scope.
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, secret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api.auth.endpoint1");
IdentityServer will lookup the Resource name and add that to the token as audience (aud) while the scope is added as claim with type scope.
This should be enough to make it work. Also check the sample project.
Custom authentication scheme and scope based policies for different access rights bundled together looks like that:
// Startup.ConfigureServices
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication("CustomAuthEndpointsAuthenticationScheme", options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth"; //IdentityServer4.Models.ApiResource.Name aka Audience
});
services.AddAuthorization(options =>
{
options.AddPolicy("Endpoint1Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint1"); } ); //IdentityServer4.Models.Scope.Name
options.AddPolicy("Endpoint2Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint2"); } ); //IdentityServer4.Models.Scope.Name
} );
// securing the custom endpoint controllers with different access rights
[Authorize(AuthenticationSchemes = "CustomAuthEndpointsAuthenticationScheme", Policy = "Endpoint1Policy")]
It seems not to interfere with the IdentityServer4 default endpoints nor with the ASP.NET Core Identity part.

What is AdalDistributedTokenCache when using OpenID Connect in ASP.NET Core 2.0?

The code shown here is my attempt to perform authentication in ASP.NET Core 2.0 against my Azure AD tenant.
The interesting part is my next set of objectives upon receiving an authentication code.
I want put the authenticated user's AD Groups into claims and have them passed along to my policy-based authorisation registrations.
To achieve this, I exchange the authorisation code for an access token.
Upon obtaining access token, I use Microsoft Graph SDK to retrieve the authenticated user's AD Groups.
Question 1: I have seen examples where the access token is stored in a cache IDistributedCache. Why is this important and what risk is there in not performing this step and what exactly is AdalDistributedTokenCache?
e.g.
var cache = new AdalDistributedTokenCache(distributedCache, userId);
var authContext = new AuthenticationContext(ctx.Options.Authority, cache);
I find the access token is always at hand via
string accessToken = await HttpContext.GetTokenAsync("access_token");
Question 2: After retrieving groups, if I add these as claims to the Principal, can I then use them to drive authorization policies as described here?
Policy-based authorisation in ASP.NET Core
Question 3: Does the access token and id token along with the claims I add end up inside the cookie?
Question 4: How can I force Azure AD to return AD Roles as claims (not groups as I can get these via Graph) without having to change some kind of manifest?
Full code
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
Configuration.GetSection("OpenIdConnect").Bind(options);
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async ctx =>
{
// Exchange authorization code for access token
var request = ctx.HttpContext.Request;
var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
var authContext = new AuthenticationContext(ctx.Options.Authority);
var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, ctx.Options.Resource);
// Use Microsoft Graph SDK to retrieve AD Groups
var email = ctx.Principal.Claims.First(f => f.Type == ClaimTypes.Upn).Value;
GraphServiceClient client = new GraphServiceClient(
new DelegateAuthenticationProvider(
async requestMessage => {
var accessToken = result.AccessToken;
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
}));
var groups = await client.Users[email].GetMemberGroups(false).Request()
.PostAsync();
// Do something with groups
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Index");
});
}
Question 1: I have seen examples where the access token is stored in a cache IDistributedCache. Why is this important and what risk is there in not performing this step and what exactly is AdalDistributedTokenCache?
ADAL uses an in-memory token cache by default where it keeps the access and refresh tokens it acquires.
By using a distributed cache backed by e.g. Redis, all of the instances hosting the app can access the token cache.
This is required if the app runs behind a load balancer, and also prevents the data from being lost when the app restarts.
Question 2: After retrieving groups, if I add these as claims to the Principal, can I then use them to drive authorization policies as described here?
You can add a new identity on the user principal, similar to my article: https://joonasw.net/view/adding-custom-claims-aspnet-core-2.
It should work if you add the identity in the OnAuthorizationCodeReceived handler.
They will be stored as claims using the default sign-in scheme, which is Cookies in your case.
So yes, you can use them in policies then.
Question 3: Does the access token and id token along with the claims I add end up inside the cookie?
Yes, they are all persisted in the cookie.
However, you should use ADAL to get the access token when you need it.
The option to save tokens is not really needed in your case, as long as you set up the ADAL token cache correctly.
Acquiring the token: https://github.com/juunas11/aspnetcore2aadauth/blob/master/Core2AadAuth/Startup.cs#L75
Using a token: https://github.com/juunas11/aspnetcore2aadauth/blob/master/Core2AadAuth/Controllers/HomeController.cs#L89
The sample app first creates a token cache for the signed-in user.
Then, we use ADAL's AcquireTokenSilentAsync method to get an access token silently.
This means ADAL will return the cached access token, or if it has expired, uses the cached refresh token to get a new access token.
If both of those fail, an exception is thrown.
In the case of the sample app, there is an exception filter that catches the exception and redirects the user to login: https://github.com/juunas11/aspnetcore2aadauth/blob/master/Core2AadAuth/Filters/AdalTokenAcquisitionExceptionFilter.cs
Question 4: How can I force Azure AD to return AD Roles as claims (not groups as I can get these via Graph) without having to change some kind of manifest?
If you mean roles like Global Administrator, you cannot get that in claims.
Roles which you define in the app manifest, and assign to users/groups are always included in the token. https://joonasw.net/view/defining-permissions-and-roles-in-aad

Asp.Net Core 2.0 and Azure AD B2C for authentication on WebApp and API

I have an existing small app that I use for test, it is in Asp.Net Core 1.1 for both the Web App and the API, the authentication is done using Azure AD B2C.
I am trying to move it to .Net Core 2.0 but I can't figure how to get it working, I tried using both sample from GitHub Azure-Samples for Web App and API, but I have either an unauthorized or 500 error when trying to access the api, if you have a working example for calling a web api from a web app using 2.0 and protected by AD B2C it will be greatly appreciated.
Edit:
The sample I use to test are :
Web App : WebApp-OpenIDConnect-DotNet core2.0
Web Api : B2C-WebApi core2.0
, I changed the appsettings values to match my b2c directory.
For my asp.net core 1.1 test app I use the same samples as above but from the master branch, with the same value for appsettings.
Edit 2
by default, in startup.cs I have this :
services.AddAuthentication()
.AddJwtBearer(option => new JwtBearerOptions
{
Authority = string.Format("https://login.microsoftonline.com/tfp/{0}/{1}/v2.0/",
Configuration["Authentication:AzureAd:Tenant"], Configuration["Authentication:AzureAd:Policy"]),
Audience = Configuration["Authentication:AzureAd:ClientId"],
Events = new JwtBearerEvents
{
OnAuthenticationFailed = AuthenticationFailed
}
});
which gives me the following error:
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:44352/api/values/5
Microsoft.AspNetCore.Server.Kestrel:Error: Connection id "0HL89JHF4VBLM", Request id "0HL89JHF4VBLM:00000001": An unhandled exception was thrown by the application.
System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
if modified services.AddAuthentication like that
services.AddAuthentication(sharedOption =>
{
sharedOption.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
the error is now
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: Failed to validate the token xxx.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10500: Signature validation failed. No security keys were provided to validate the signature.
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.d__6.MoveNext()
I saw a pull request on the sample which correct this issue (Link), the services.AddAuthentication must be change to:
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions =>
{
jwtOptions.Authority = $"https://login.microsoftonline.com/tfp/{Configuration["Authentication:AzureAd:Tenant"]}/{Configuration["Authentication:AzureAd:Policy"]}/v2.0/";
jwtOptions.Audience = Configuration["Authentication:AzureAd:ClientId"];
jwtOptions.Events = new JwtBearerEvents
{
OnAuthenticationFailed = AuthenticationFailed
};
});
I got this example working both for Core 1.1 and Core 2.0, please add the Oath Authentication as below,
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
You configuration options will be defined inside of the class "AzureAdB2CAuthenticationBuilderExtensions", which is found inside of the
azure b2c project
Looks like your token is not being update it from the Azure, are you able to get the token from your web app? could you please verify that you are not getting null
Did you register your api scopes on your azure b2c tenant web app?
"ApiScopes": "https://fabrikamb2c.onmicrosoft.com/demoapi/demo.read"
you have to set scope in your web api and allows to be read on the web app, please follow click the link in order to set it up