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
Related
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.
I have implemented JWT tokens to know who the current user is in an API application that is being used by an MVC controller.
I'm building something like a forum app. The user must be logged in to be able to post, so I'm basically trying to use the JWT token to store the current user's email.
When the user click on "Create Post" the action should get the token and its value, the problem is that I don't know how to use the token to protect controllers or retrieve data from current user, I have already copied and pasted the token in jwt.io to check if the data is stored correctly in the token and the value (the user's email) is stored correctly.
The API controller with the "login" action:
public async Task<IActionResult> login([FromBody] Usuario model)
{
//check if user exists and the password is correct
//generates the token
var SecretKey = config.GetValue<string>("SecretKey");
var key = Encoding.ASCII.GetBytes(SecretKey);
var claims = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Mail)
});
claims.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Mail));
var tokenDesc = new SecurityTokenDescriptor
{
Subject = claims,
Expires = DateTime.UtcNow.AddMinutes(20),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var createdToken = tokenHandler.CreateToken(tokenDesc);
string bearer_token = tokenHandler.WriteToken(createdToken);
using(var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer" + bearer_token);
}
return Ok(bearer_token);
}
}
The MVC controller from where the API is used:
public async Task<IActionResult> login(Usuario model)
{
HttpClient hc = new HttpClient();
hc.BaseAddress = new Uri("https://localhost:44325/api/Usuarios/");
var login = await hc.PostAsJsonAsync<Usuario>("login", model);
//check the response
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Name, model.Email));
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
HttpContext.Session.SetString("JWToken", login.ToString());
hc.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", login.ToString());
return RedirectToAction("IndexForumList", "ForumControllerMVC");
}
}
This is the API method to "Create Posts" and where the token should be used, here the userId is null:
public async Task<IActionResult> createPost([FromForm]ForumModel model)
{
string userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
//retrieves the current user email, validates and save the content to database
}
And this is the MVC method to "Create Posts":
public async Task<IActionResult> createPost(ForumModel model)
{
HttpClient hc = new HttpClient();
hc.BaseAddress = new Uri("https://localhost:44325/api/Usuarios/");
//var userPost = hc.PostAsJsonAsync<ForumModel>("Usuarios/createPost", model);
var userPost = await hc.PostAsync("createPost", formContent);
if(userPost.IsSuccessStatusCode == true)
{
return RedirectToAction("IndexForumList", "ForoControllerMVC");
}
}
I have been suffering with this due to my lack of knowledge about JWT, any help is appreciated.
UPDATE
The startup.cs
public void ConfigureServices(IServiceCollection services)
{
var key = Encoding.ASCII.GetBytes(Configuration.GetValue<string>("SecretKey"));
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSession(
options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
If I understand your question correctly,
To protect your API you can decorate it with [Authorize] attribute. Eg -
[Authorize]
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
And to validate your tokens since you are using .netcore for your api, you ll have to create a middleware that will validate the token before your requests hit the API endpoint. You can follow this tutorial for more details on how to use JWT with ASP.NET core.
To get user Id in your case, you ll have to validate the token first and then extract the UserId. Try changing your code in createPost api to this -
public async Task<IActionResult> createPost([FromForm]ForumModel model)
{
var tokenHandler = new JwtSecurityTokenHandler();
var SecretKey = config.GetValue<string>("SecretKey");
var key = Encoding.ASCII.GetBytes(SecretKey);
var token = HttpContext.Request.Headers["Authorization"];
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "NameIdentifier").Value);
}
Although this should be handled in the middleware and you can then attach the authenticated user to the current HttpContext.Items collection to make it accessible within the scope of the current request. All this is explained in the tutorial in detail. Hope that helps.!
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.
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
});
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)