implementing roles in identity server 4 with asp.net identity - c#

I am working on an asp.net MVC application with identity server 4 as token service. I have an api as well which has some secure resources. I want to implement roles (Authorization) for api. I want to make sure that only an authorized resource with valid role can access an api end point otherwise get 401 (unauthorized error).
Here are my configurations:
Client
new Client()
{
ClientId = "mvcClient",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = new List<Secret>()
{
new Secret("secret".Sha256())
},
RequireConsent = false;
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002" },
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
StandardScopes.Roles.Name,
"API"
}
}
Scopes
return new List<Scope>()
{
StandardScopes.OpenId, // subject id
StandardScopes.Profile, // first name, last name
StandardScopes.OfflineAccess, // requesting refresh tokens for long lived API access
StandardScopes.Roles,
new Scope()
{
Name = "API",
Description = "API desc",
Type = ScopeType.Resource,
Emphasize = true,
IncludeAllClaimsForUser = true,
Claims = new List<ScopeClaim>
{
new ScopeClaim(ClaimTypes.Name),
new ScopeClaim(ClaimTypes.Role)
}
}
};
User
new InMemoryUser()
{
Subject = "1",
Username = "testuser",
Password = "password",
Claims = new List<Claim>()
{
new Claim("name", "Alice"),
new Claim("Website", "http://alice.com"),
new Claim(JwtClaimTypes.Role, "admin")
}
}
and in server startup i added this:
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddSigningCredential(cert)
.AddInMemoryClients(Config.GetClients())
.AddInMemoryScopes(Config.GetScopes())
.AddInMemoryUsers(Config.GetUsers())
in api startup, i have this:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions()
{
Authority = "http://localhost:5000",
ScopeName = "NamfusAPI",
RequireHttpsMetadata = false
});
in api controller, i have this:
[Authorize(Roles = "admin")]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new {c.Type, c.Value });
}
in MVC client startup, i have this:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "Cookies"
});
var oidcOptions = new OpenIdConnectOptions()
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ClientId = "mvcClient",
ClientSecret = "secret",
SaveTokens = true,
GetClaimsFromUserInfoEndpoint = true,
ResponseType = "code id_token", // hybrid flow
};
oidcOptions.Scope.Clear();
oidcOptions.Scope.Add("openid");
oidcOptions.Scope.Add("profile");
oidcOptions.Scope.Add("NamfusAPI");
oidcOptions.Scope.Add("offline_access");
oidcOptions.Scope.Add("roles");
I am trying to call the api like this:
public async Task<IActionResult> CallApiUsingUserAccessToken()
{
var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(accessToken);
var content = await client.GetStringAsync("http://localhost:5001/identity");
ViewBag.Json = JArray.Parse(content).ToString();
return View("json");
}
I get access token but when call is made to api (identity/get), I get 302 error Forbidden (in chrome network it shows 500 internal server error). If I change API Authorize attribute from
[Authorize(Roles = "admin")]
public IActionResult Get()
to (without role):
[Authorize]
public IActionResult Get()
it works and I get data from api in mvc app. How can I apply roles in this code.
Please suggest.

First, you need to request "API" scope in your OpenIdConnectOptions().
oidcOptions.Scope.Add("API");
or
Scope = { "API", "offline_access",..},
Then you need to check if the role claim is included in the claims list available to your API controler(don't apply the roles filter in authorize attribute yet. Put a debug point inside controller method and expand User property). Check if the type of the role claim you received(listed in Claims Collection) matches User.Identity.RoleClaimType property
If the role claim type you have and User.Identity.RoleClaimType doesn't match, authorize attribute with roles filter won't work. You can set the correct RoleClaimType in IdentityServerAuthenticationOptions() like follows
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5000",
ScopeName = "API",
RoleClaimType = ClaimTypes.Role,
RequireHttpsMetadata = false
});

Related

Logout from ASP.NET MVC client app and a .NET 5 openiddict server. Post logout redirect url invalid

I'm using OWIN 4.2 with .NET Framework 4.7.2 for my ASP.NET MVC client app.
Login works completely fine but logout will fail.
On my client's startup.cs
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = "MVC",
ClientSecret = "MVC-Secret",
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
CallbackPath = new PathString("/"),
Scope = "openid api",
SignInAsAuthenticationType = "cookie",
RequireHttpsMetadata = false,
UseTokenLifetime = false,
RedeemCode = true,
SaveTokens = true,
ResponseType = OpenIdConnectResponseType.Code,
ResponseMode = OpenIdConnectResponseMode.Query,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to the OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
// generate code verifier and code challenge
var codeVerifier = CryptoRandom.CreateUniqueId(32);
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Base64Url.Encode(challengeBytes);
}
// set code_challenge parameter on authorization request
n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
n.ProtocolMessage.SetParameter("code_challenge_method", "S256");
// remember code verifier in cookie (adapted from OWIN nonce cookie)
// see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L85
RememberCodeVerifier(n, codeVerifier);
}
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
return Task.CompletedTask;
},
AuthorizationCodeReceived = n =>
{
// get code verifier from cookie
// see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L102
var codeVerifier = RetrieveCodeVerifier(n);
// attach code_verifier on token request
n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
return Task.CompletedTask;
},
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
id,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
}
}
);
I also tried
...
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
PostLogoutRedirectUri = "https://localhost:44347/signout-callback-oidc",
...
And also
...
Authority = "https://localhost:44305/",
RedirectUri = "https://localhost:44347/",
PostLogoutRedirectUri = "https://localhost:44347/",
...
However, all these results in the response
error:invalid_request
error_description:The specified 'post_logout_redirect_uri' is invalid.
error_uri:https://documentation.openiddict.com/errors/ID2052
On my server, the configuration is as follows
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = clientId,
ClientSecret = clientSecret,
DisplayName = displayName,
RedirectUris =
{
new Uri("https://localhost:44347/")
},
Permissions =
{
...
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44347/")
}
}, cancellationToken);
}
I have also tried changing Server config to
PostLogoutRedirectUris =
{
new Uri("https://localhost:44347/signout-callback-oidc")
}
I encountered the same issue, what solved it for me was to add the logout permission in the application - OpenIddictConstants.Permissions.Endpoints.Logout
await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
DisplayName = "MVC client application",
PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUris = { new Uri("http://localhost:53507/signin-oidc") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode
}
});
As answered by Apps in here https://stackoverflow.com/a/69671657/6477254, I can confirm that you must allow permission for the logout endpoint, using the constant value of OpenIddictConstants.Permissions.Endpoints.Logout, which hold "ept:logout" string value when creating the OpenIddict data.

Identity Server: Add claims to access token in hybrid flow in MVC client

I've read the docs and followed the examples but I am unable to get user claims into the access token. My client is not ASP.NET core, so the configuration of the MVC client is not the same as the v4 samples.
Unless I have misunderstood the docs, the ApiResources are used to populate the RequestedClaimTypes in the profile service when creating the access token. The client should add the api resource to it's list of scopes to include associated userclaims. In my case they are not being connected.
When ProfileService.GetProfileDataAsync is called with a caller of "ClaimsProviderAccessToken", the requested claim types are empty. Even if I set the context.IssuedClaims in here, when it is called again for "AccessTokenValidation" the claims on the context are not set.
In the MVC app:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
UseTokenLifetime = false,
ClientId = "portal",
ClientSecret = "secret",
Authority = authority,
RequireHttpsMetadata = false,
RedirectUri = redirectUri,
PostLogoutRedirectUri = postLogoutRedirectUri,
ResponseType = "code id_token",
Scope = "openid offline_access portal",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
await AssembleUserClaims(n);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
private static async Task AssembleUserClaims(AuthorizationCodeReceivedNotification notification)
{
string authCode = notification.ProtocolMessage.Code;
string redirectUri = "https://myuri.com";
var tokenClient = new TokenClient(tokenendpoint, "portal", "secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(authCode, redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(new Uri(userinfoendpoint), tokenResponse.AccessToken);
var userInfoResponse = await userInfoClient.GetAsync();
// create new identity
var id = new ClaimsIdentity(notification.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", notification.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", notification.AuthenticationTicket.Identity.FindFirst("sid").Value));
notification.AuthenticationTicket = new AuthenticationTicket(id, notification.AuthenticationTicket.Properties);
}
Identity Server Client:
private Client CreatePortalClient(Guid tenantId)
{
Client portal = new Client();
portal.ClientName = "Portal MVC";
portal.ClientId = "portal";
portal.ClientSecrets = new List<Secret> { new Secret("secret".Sha256()) };
portal.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
portal.RequireConsent = false;
portal.RedirectUris = new List<string> {
"https://myuri.com",
};
portal.AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"portal"
};
portal.Enabled = true;
portal.AllowOfflineAccess = true;
portal.AlwaysSendClientClaims = true;
portal.AllowAccessTokensViaBrowser = true;
return portal;
}
The API resource:
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource
{
Name= "portalresource",
UserClaims = { "tenantId","userId","user" },
Scopes =
{
new Scope()
{
Name = "portalscope",
UserClaims = { "tenantId","userId","user",ClaimTypes.Role, ClaimTypes.Name),
},
}
},
};
}
The Identity resource:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
// some standard scopes from the OIDC spec
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("portal", new List<string>{ "tenantId", "userId", "user", "role", "name"})
};
}
UPDATE:
Here is the interaction between the MVC app and the Identity Server (IS):
MVC:
Owin Authentication Challenge
IS:
AccountController.LoginAsync - assemble user claims and call HttpContext.SignInAsync with username and claims)
ProfileService.IsActiveAsync - Context = "AuthorizeEndpoint", context.Subject.Claims = all userclaims
ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 1 IdentityResource (OpenId), GrantType = Hybrid
MVC:
SecurityTokenValidated (Notification Callback)
AuthorizationCodeReceived - Protocol.Message has Code and IdToken call to TokenClient.RequestAuthorizationCodeAsync()
IS:
ProfileService.IsActiveAsync - Context = "AuthorizationCodeValidation", context.Subject.Claims = all userclaims
ClaimsService.GetAccessTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = Hybrid
ProfileService.GetProfileDataAsync - Context = "ClaimsProviderAccessToken", context.Subject.Claims = all userclaims, context.RequestedClaimTypes = empty, context.IssuedClaims = name,role,user,userid,tenantid
ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = authorization_code
MVC:
call to UserInfoClient with tokenResponse.AccessToken
IS:
ProfileService.IsActiveAsync - Context = "AccessTokenValidation", context.Subject.Claims = sub,client_id,aud,scope etc (expecting user and tenantId here)
ProfileService.IsActiveAsync - Context = "UserInfoRequestValidation", context.Subject.Claims = sub,auth_time,idp, amr
ProfileService.GetProfileDataAsync - Context = "UserInfoEndpoint", context.Subject.Claims = sub,auth_time,idp,amp, context.RequestedClaimTypes = sub
As I'm not seeing what happens in your await AssembleUserClaims(context); I would suggest to check if it is doing the following:
Based on the the access token that you have from either the context.ProtoclMessage.AccessToken or from the call to the TokenEndpoint you should create a new ClaimsIdentity. Are you doing this, because you are not mentioning it?
Something like this:
var tokenClient = new TokenClient(
IdentityServerTokenEndpoint,
"clientId",
"clientSecret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaims(n.AuthenticationTicket.Identity.Claims);
// get user info claims and add them to the identity
var userInfoClient = new UserInfoClient(IdentityServerUserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var userInfoEndpointClaims = userInfoResponse.Claims;
// this line prevents claims duplication and also depends on the IdentityModel library version. It is a bit different for >v2.0
id.AddClaims(userInfoEndpointClaims.Where(c => id.Claims.Any(idc => idc.Type == c.Type && idc.Value == c.Value) == false));
// create the authentication ticket
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
And one more thing - read this regarding the resources. In your particular case, you care about IdentityResources (but I see that you also have it there).
So - when calling the UserInfoEndpoint do you see the claims in the response? If no - then the problem is that they are not issued.
Check these, and we can dig in more.
Good luck
EDIT
I have a solution that you may, or may not like, but I'll suggest it.
In the IdentityServer project, in the AccountController.cs there is a method public async Task<IActionResult> Login(LoginInputModel model, string button).
This is the method after the user has clicked the login button on the login page (or whatever custom page you have there).
In this method there is a call await HttpContext.SignInAsync. This call accept parameters the user subject, username, authentication properties and list of claims. Here you can add your custom claim, and then it will appear when you call the userinfo endpoint in the AuthorizationCodeReceived. I just tested this and it works.
Actually I figured out that this is the way to add custom claims. Otherwise - IdentityServer doesn't know about your custom claims, and is not able to populate them with values. Try it out and see if it works for you.
You need to modify the code of "Notifications" block in MVC App like mentioned below:
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n => {
var userInfoClient = new UserInfoClient(UserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(n.ProtocolMessage.AccessToken);
var identity = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
identity.AddClaims(userInfoResponse.Claims);
var tokenClient = new TokenClient(TokenEndpoint, "portal", "secret");
var response = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
identity.AddClaim(new Claim("access_token", response.AccessToken));
identity.AddClaim(new Claim("expires_at", DateTime.UtcNow.AddSeconds(response.ExpiresIn).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
identity.AddClaim(new Claim("refresh_token", response.RefreshToken));
identity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(identity, n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
return Task.FromResult(0);
}
}
(consider if any changes related to the version of identity server as this code was built for identity server 3.)
Why do you have "portal" listed as an identity resource and Api resource? That could be causing some confusion.
Also, before I switched to IdentityServer4 and asp.net core, my IdentityServer3 startup code looked very similar to what you have with MVC. You may want to look at the examples for IdentityServer3.
Some suggestions I may give, in your "ResponseType" field for MVC, you could try "code id_token token"
Also, you are setting your claims on AuthorizationCodeReceived, instead use SecurityTokenValidated.
But you shouldn't have to do anything custom like people are mentioning. IdentityServer4 handles custom ApiResources like you are attempting to do.
You can try to implement your own IProfileService and override it following way:
services.AddIdentityServer()
.//add clients, scopes,resources here
.AddProfileService<YourOwnProfileProvider>();
For more information look up here:
https://damienbod.com/2016/10/01/identityserver4-webapi-and-angular2-in-a-single-asp-net-core-project/
portal is not an identity resource: you should remove
new IdentityResource("portal", new List{ "tenantId",
"userId", "user", "role", "name"})
Names for the api resources should be consistent:
public static IEnumerable GetApiResources()
{
return new List
{
new ApiResource
{
Name= "portal",
UserClaims = { "tenantId","userId","user" },
Scopes =
{
new Scope("portal","portal")
}
},
};
}
Try setting GrantTypes.Implicit in the client.

Getting claims in identity server using resource owner password

I am using identity server 4 for authentication using grant type as 'ResourceOwnerPassword'. I am able to authenticate the user but not able to get claims related to user. So how can I get those ?
Below is my code
Client
Startup.cs
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ApiName = "api1"
});
Controller
public async Task<IActionResult> Authentication(LoginViewModel model)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(model.Email, model.Password, "api1");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
}
// Here I am not getting the claims, it is coming Forbidden
var extraClaims = new UserInfoClient(disco.UserInfoEndpoint);
var identityClaims = await extraClaims.GetAsync(tokenResponse.AccessToken);
if (!tokenResponse.IsError)
{
Console.WriteLine(identityClaims.Json);
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
}
Server
Startup.cs
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<IdentityProfileService>()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
Config.cs
public static IEnumerable<Client> GetClients(IConfigurationRoot Configuration)
{
// client credentials client
return new List<Client>
{
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
AccessTokenType = AccessTokenType.Jwt
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
But when I check my access token in jwt.io there I can see the claims But why I am not able to get in the controller ?
Any help on this appreciated !
You can call the UserInfoEndpoint, as per your example, but you can also get additional claims if you define your ApiResource as requiring them.
For example, rather than just defining your ApiResource like you are:
new ApiResource("api1", "My API")
You can use the expanded format and define what UserClaims you'd like to have when getting an access token for this scope.
For example:
new ApiResource
{
Name = "api1",
ApiSecrets = { new Secret(*some secret*) },
UserClaims = {
JwtClaimTypes.Email,
JwtClaimTypes.PhoneNumber,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
JwtClaimTypes.PreferredUserName
},
Description = "My API",
DisplayName = "MyApi1",
Enabled = true,
Scopes = { new Scope("api1") }
}
Then in your own implementation of the IProfileService you will find that calls to GetProfileDataAsync have a list of what claims are requested in the context (ProfileDataRequestContext.RequestedClaimTypes). Given that list of what's been asked for, you can then add any claims you like - however you like - to the context.IssuedClaims that you return from that method. These will then be a part of the access token.
If you only want certain claims by specifically calling the UserInfo endpoint though, you'll want to create an IdentityResource definition and have that scope included as part of your original token request.
For example:
new IdentityResource
{
Name = "MyIdentityScope",
UserClaims = {
JwtClaimTypes.EmailVerified,
JwtClaimTypes.PhoneNumberVerified
}
}
But your first problem is following the other answer here so you don't get 'forbidden' as the response to the UserInfo endpoint!
Try sending the token along the request, when calling the UserInfoEndpoint. Try this:
var userInfoClient = new UserInfoClient(doc.UserInfoEndpoint, token);
var response = await userInfoClient.GetAsync();
var claims = response.Claims;
official docs

OWIN OpenID connect authorization fails to authorize secured controller / actions

I am working on a project where a third party provider will act as an Oauth2 based Authorization Server. An Asp.net MVC 5 based client which will send the user to the authorization server to authenticate (using login / password) and the auth server will return an access token back to the MVC client. Any further calls to resource servers (APIs) will be made using the access token.
To achieve this I am using Microsoft.Owin.Security.OpenIdConnect and the UseOpenIdConnectAuthentication extension. I am able to successfully redirect and get the access token from the auth server but the client is not creating an Authentication Cookie. Every time I try to access a secured page, I get the callback page with access token.
What am I missing here? My current code is below.
The secured controller action:
namespace MvcWebApp.Controllers
{
public class SecuredController : Controller
{
// GET: Secured
[Authorize]
public ActionResult Index()
{
return View((User as ClaimsPrincipal).Claims);
}
}
}
The Startup Class:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType("ClientCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = "ClientCookie",
CookieName = CookieAuthenticationDefaults.CookiePrefix + "ClientCookie",
ExpireTimeSpan = TimeSpan.FromMinutes(5)
});
// ***************************************************************************
// Approach 1 : ResponseType = "id_token token"
// ***************************************************************************
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),
Authority = "https://thirdparty.com.au/oauth2",
ClientId = "_Th4GVMa0JSrJ8RKcZrzbcexk5ca",
ClientSecret = "a3GVJJbLHkrn9nJRj3IGNvk5eGQa",
RedirectUri = "http://mvcwebapp.local/",
ResponseType = "id_token token",
Scope = "openid",
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://thirdparty.com.au/oauth2/authorize",
TokenEndpoint = "https://thirdparty.com.au/oauth2/token",
UserInfoEndpoint = "https://thirdparty.com.au/oauth2/userinfo",
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var token = n.ProtocolMessage.AccessToken;
// persist access token in cookie
if (!string.IsNullOrEmpty(token))
{
n.AuthenticationTicket.Identity.AddClaim(
new Claim("access_token", token));
}
return Task.FromResult(0);
},
AuthenticationFailed = notification =>
{
if (string.Equals(notification.ProtocolMessage.Error, "access_denied", StringComparison.Ordinal))
{
notification.HandleResponse();
notification.Response.Redirect("/");
}
return Task.FromResult<object>(null);
}
}
});
// ***************************************************************************
// Approach 2 : ResponseType = "code"
// ***************************************************************************
//app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
//{
// AuthenticationMode = AuthenticationMode.Active,
// AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
// SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),
// Authority = "https://thirdparty.com.au/oauth2",
// ClientId = "_Th4GVMa0JSrJ8RKcZrzbcexk5ca",
// ClientSecret = "a3GVJJbLHkrn9nJRj3IGNvk5eGQa",
// RedirectUri = "http://mvcwebapp.local/",
// ResponseType = "code",
// Scope = "openid",
// Configuration = new OpenIdConnectConfiguration
// {
// AuthorizationEndpoint = "https://thirdparty.com.au/oauth2/authorize",
// TokenEndpoint = "https://thirdparty.com.au/oauth2/token",
// UserInfoEndpoint = "https://thirdparty.com.au/oauth2/userinfo",
// },
// Notifications = new OpenIdConnectAuthenticationNotifications
// {
// AuthorizationCodeReceived = async (notification) =>
// {
// using (var client = new HttpClient())
// {
// var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
// var request = new HttpRequestMessage(HttpMethod.Get, configuration.TokenEndpoint);
// request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
// {
// {OpenIdConnectParameterNames.ClientId, notification.Options.ClientId},
// {OpenIdConnectParameterNames.ClientSecret, notification.Options.ClientSecret},
// {OpenIdConnectParameterNames.Code, notification.ProtocolMessage.Code},
// {OpenIdConnectParameterNames.GrantType, "authorization_code"},
// {OpenIdConnectParameterNames.ResponseType, "token"},
// {OpenIdConnectParameterNames.RedirectUri, notification.Options.RedirectUri}
// });
// var response = await client.SendAsync(request, notification.Request.CallCancelled);
// response.EnsureSuccessStatusCode();
// var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
// // Add the access token to the returned ClaimsIdentity to make it easier to retrieve.
// notification.AuthenticationTicket.Identity.AddClaim(new Claim(
// type: OpenIdConnectParameterNames.AccessToken,
// value: payload.Value<string>(OpenIdConnectParameterNames.AccessToken)));
// }
// }
// }
//});
}
}
TL;DR: use ResponseType = "id_token token" and it should work.
In OpenID Connect, response_type=token is not considered as a legal value: http://openid.net/specs/openid-connect-core-1_0.html#Authentication.
Sometimes implemented for backward compatibility reasons, response_type=token is not supported by the OIDC middleware developed by MSFT: an exception is always thrown when no id_token is returned by the OpenID Connect provider (which also excludes the valid code flow). You can find more information on this other SO post.
(remark: in SecurityTokenValidated, you're replacing the ticket created by the OIDC middleware using n.AuthenticationTicket = new AuthenticationTicket(...): it's not the recommended approach and will result in a ClaimsIdentity missing the essential claims. You should consider removing the assignation and simply add new claims like you do for the access_token claim)

Use Bearer Token Authentication for API and OpenId authentication for MVC on the same application project

I am trying to use both OpenId and Bearer token authentication on my application through Identity Server.
The problem currently is that once I have authenticated the user, I still need to get a bearer token to be able to call any action methods for my Asp.Net MVC application.
Here is my startup file for the application
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
}
});
app.UseResourceAuthorization(new AuthorizationManager());
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi" }
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
I would like to restrict bearer token authentication to my api urls only, and use openID auth for everthing else. Is there a way to do that?
Ok, I found some information on the following post
https://github.com/IdentityServer/IdentityServer3/issues/487
The github repo that implements the concepts discussed in the link can be found here
https://github.com/B3nCr/IdentityServer-Sample/blob/master/B3nCr.Communication/Startup.cs
Basically you need to map the api url to a different configuration using app.Map(). In my case, I changed my startup file to look like this.
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
var openIdConfig = new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.GivenName,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
n.Request.Headers.SetValues("Authorization ", new string[] { "Bearer ", n.ProtocolMessage.AccessToken });
}
}
};
app.UseOpenIdConnectAuthentication(openIdConfig);
app.UseResourceAuthorization(new AuthorizationManager());
app.Map("/api", inner =>
{
var bearerTokenOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi" }
};
inner.UseIdentityServerBearerTokenAuthentication(bearerTokenOptions);
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
inner.UseWebApi(config);
});
}
}
That solved my problem. I can now access the MVC pages with cookies based authentication, and call the API with bearer token authentication.

Categories