Short: My client retrieves an access token from IdentityServer sample server, and then passes it to my WebApi. In my controller, this.HttpContext.User.GetUserId() returns null (User has other claims though). I suspect access token does not have nameidentity claim in it. How do I make IdentityServer include it?
What I've tried so far:
switched from hybrid to implicit flow (random attempt)
in IdSvrHost scope definition I've added
Claims = { new ScopeClaim(ClaimTypes.NameIdentifier, alwaysInclude: true) }
in IdSvrHost client definition I've added
Claims = { new Claim(ClaimTypes.NameIdentifier, "42") }
(also a random attempt)
I've also tried other scopes in scope definition, and neither of them appeared. It seems, that nameidentity is usually included in identity token, but for most public APIs I am aware of, you don't provide identity token to the server.
More details:
IdSrvHost and Api are on different hosts.
Controller has [Authorize]. In fact, I can see other claims coming.
Api is configured with
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseIdentityServerAuthentication(options => {
options.Authority = "http://localhost:22530/";
// TODO: how to use multiple optional scopes?
options.ScopeName = "borrow.slave";
options.AdditionalScopes = new[] { "borrow.receiver", "borrow.manager" };
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
});
Scope:
public static Scope Slave { get; } = new Scope {
Name = "borrow.slave",
DisplayName = "List assigned tasks",
Type = ScopeType.Resource,
Claims = {
new ScopeClaim(ClaimTypes.NameIdentifier, alwaysInclude: true),
},
};
And client:
new Client {
ClientId = "borrow_node",
ClientName = "Borrow Node",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"borrow_node:redirect-target",
},
Claims = { new Claim(ClaimTypes.NameIdentifier, "42") },
AllowedScopes = {
StandardScopes.OpenId.Name,
//StandardScopes.OfflineAccess.Name,
BorrowScopes.Slave.Name,
},
}
Auth URI:
request.CreateAuthorizeUrl(
clientId: "borrow_node",
responseType: "token",
scope: "borrow.slave",
redirectUri: "borrow_node:redirect-target",
state: state,
nonce: nonce);
and I also tried
request.CreateAuthorizeUrl(
clientId: "borrow_node",
responseType: "id_token token",
scope: "openid borrow.slave",
redirectUri: "borrow_node:redirect-target",
state: state,
nonce: nonce);
Hooray, I found an answer, when I stumbled upon this page: https://github.com/IdentityServer/IdentityServer3.Samples/issues/173
Apparently, user identity is passed in "sub" claim in the access token. Because I blindly copied API sample, its configuration included
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
which essentially prevented my API from mapping "sub" claim to nameidentifier. After removing this line, HttpContext.User.GetUserId() of authenticated controller returns user ID correctly.
Related
We have just upgraded our web app to .NET 5 and IdentityServer4 to V4, also we switched from hybrid flow to code + PKCE. The client is set to an access token type of reference, also the client uses bearer tokens for an internal API as well as cookies for the main website.
When we deploy to our internal development server (IIS 8.5) or to Azure App Services randomly when we ask for the access token (reference) we are returned an access token (JWT) instead. We did use the httpContext.GetTokenAsync() method but then replaced it with the identitymodel.aspnetcore GetUserAccessTokenAsync() method but it still returns a JWT Token.
I have validated the JWT tokens contents and they are the relevant user and their claims. I have also checked the persisted grant table and the reference token entered in there specifies it as a JWT instead of Reference.
The only way to rectify the situation is
Stop the website and identityserver
Clear the cookies in the browser
Delete all entries in the persisted grants table
Recycle the app pools
Start the identity server and then perform a login
Start the website which the login is for and suddenly we get a reference access token again
Identity Server Client Config
AllowedGrantTypes =
{
GrantType.AuthorizationCode,
"exchange_reference_token"
},
AccessTokenType = AccessTokenType.Reference,
AccessTokenLifetime = 86400,
RequireConsent = false,
AllowAccessTokensViaBrowser = true,
ClientSecrets =
{
new Secret("*******)
},
RedirectUris = { $"{client}/signin-oidc" },
PostLogoutRedirectUris = { $"{client}/signout-callback-oidc" },
FrontChannelLogoutUri = $"{client}/signout-oidc",
AllowedCorsOrigins = { client },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"roles",
"API1",
"API2",
"API3",
"Signalr"
},
UpdateAccessTokenClaimsOnRefresh = true,
AllowOfflineAccess = true
I've found out what is causing the issue. We are using signalr in the website and we are having to use the exchange_reference_token, however in the exchange code it forces the access token to JWT, this then gets inserted in the persisted grant table causing any future request to return JWT instead of of reference access token
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var referenceToken = context.Request.Raw.Get("token");
if (referenceToken == null)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Missing Reference Token");
}
var result = await _validator.ValidateAccessTokenAsync(referenceToken);
if (result == null)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid Reference Token");
}
// Generate the JWT as if it was for the reference token's client
context.Request.Client.AccessTokenType = AccessTokenType.Jwt;
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType, result.Claims);
}
I am using aspnet core 5.0 webapi with CQRS in my project and already have jwt implementation. Not using role management from aspnet core but manually added for aspnet users table role field and it is using everywhere. In internet I can't find any article to implement keycloak for existing authentication and authorization. My point is for now users login with their email+password, idea is not for all but for some users which they already stored in keycloak, or for some users we will store there, give option login to our app using keycloak as well.
Scenario 1:
I have admin#gmail.com in both in my db and in keycloak and both are they in admin role, I need give access for both to login my app, first scenario already working needs implement 2nd scenarion beside first.
Found only this article which implements securing app (as we have already and not trying to replace but extend)
Medium keycloak
My jwt configuration looks like:
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services,
IConfiguration configuration)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
};
opt.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
return services;
}
My jwt service looks like:
public JwtGenerator(IConfiguration config)
{
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetSection("AppSettings:Token").Value));
}
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Role, user.Role.ToString("G").ToLower())
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
My login method looks like:
public async Task<GetToken> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null)
throw new BadRequestException("User not found");
UserManagement.ForbiddenForLoginUser(user);
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (result.Succeeded)
{
user.IsRoleChanged = false;
RefreshToken refreshToken = new RefreshToken
{
Name = _jwtGenerator.GenerateRefreshToken(),
DeviceName = $"{user.UserName}---{_jwtGenerator.GenerateRefreshToken()}",
User = user,
Expiration = DateTime.UtcNow.AddHours(4)
};
await _context.RefreshTokens.AddAsync(refreshToken, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return new GetToken(_jwtGenerator.CreateToken(user),refreshToken.Name);
}
throw new BadRequestException("Bad credentials");
}
My authorization handler:
public static IServiceCollection AddCustomMvc(this IServiceCollection services)
{
services.AddMvc(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
// Build the intermediate service provider
opt.Filters.Add<CustomAuthorizationAttribute>();
}).AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<CreateProjectCommand>());
return services;
}
What is best practise to implement keycloak authentiaction+authorization beside my current approach and give users to login with two scenarios, normal and keycloak login.
P.S. Ui is different and we are using angular this one just webapi for backend.
Since your login method returns a jwt, you could configure multiple bearer tokens by chaining .AddJwtBearer(), one for your normal login and one for keycloak.
Here is a link to a question that might solve your problem: Use multiple jwt bearer authentication.
Keycloak configuration:
Go to Roles -> Realm Roles and create a corresponding role.
Go to Clients -> Your client -> Mappers.
Create a new role mapper and select "User Realm Role" for Mapper Type, "roles" for Token Claim Name and "String" for Claim JSON Type. Without the mapping the role configured before would be nested somewhere else in the jwt.
You can use the debugger at jwt.io to check if your token is correct. The result should look like this:
{
"exp": 1627565901,
"iat": 1627564101,
"jti": "a99ccef1-afa9-4a62-965b-15e8d33de7de",
// [...]
// roles nested in realm_access :(
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
},
// [...]
// your mapped roles in your custom claim
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
// [...]
}
I am trying to get access token for microsoft graph api as well as One note while SSO login.
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// The `Authority` represents the v2.0 endpoint - https://login.microsoftonline.com/common/v2.0
Authority = AuthenticationConfig.Authority,
ClientId = AuthenticationConfig.ClientId,
RedirectUri = AuthenticationConfig.RedirectUri,
PostLogoutRedirectUri = AuthenticationConfig.RedirectUri,
Scope = AuthenticationConfig.BasicSignInScopes + " Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite Files.ReadWrite.All Files.ReadWrite.AppFolder Files.ReadWrite.Selected Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp User.Read User.Read.All", // a basic set of permissions for user sign in & profile access "openid profile offline_access"
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
// In a real application you would use IssuerValidator for additional checks, like making sure the user's organization has signed up for your app.
//IssuerValidator = (issuer, token, tvp) =>
//{
// if(mycustomtenantvalidation(issuer))
// return issuer;
// else
// throw new securitytokeninvalidissuerexception("invalid issuer");
// },
//NameClaimType = "name",
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Handling SameSite cookie according to https://learn.microsoft.com/en-us/aspnet/samesite/owin-samesite
CookieManager = new SameSiteCookieManager(
new SystemWebCookieManager())
The access token I am getting is working for One drive only. For one note getting request does not contain valid access token
Getting Access Token:-
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
// Upon successful sign in, get the access token & cache it using MSAL
IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { "Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite Files.ReadWrite.All Files.ReadWrite.AppFolder Files.ReadWrite.Selected Notes.Create Notes.Read Notes.Read.All Notes.ReadWrite Notes.ReadWrite.All Notes.ReadWrite.CreatedByApp User.Read User.Read.All" }, context.Code).ExecuteAsync();
var code = context.Code;
}
I have checked that we need to pass the resource id for one note while login. But I am not getting where to pass that.
I read in IDS4's tutorial that the following is a way to obtain a token.
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
if (tokenResponse.IsError) { ... }
Console.WriteLine(tokenResponse.Json);
However, it's indicated in by the intellisense that it's about to be declared as obsolete so I checked out the docs for identity model password grant type suggesting the following.
var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = "https://localhost:44300/connect/token",
ClientId = "spa",
ClientSecret = "spa_secret",
Scope = "MemberApi",
UserName = user,
Password = pass
});
I'm not certain what to do next in terms of producing a token. In the object I'm getting, there are AccessToken, IdentityToken, RefreshToken etc. and I'm confused as to which to rely on as the documentation on the differences between those is vague.
As the call goes now, I get an error saying that the client is unauthorized and the tokens are null. The client is declared as follows.
new Client
{
ClientId = "spa",
ClientSecrets = { new Secret("spa_secret".Sha256()) },
ClientName = "SPA client",
ClientUri = "http://identityserver.io",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:5000/security/credentials" },
PostLogoutRedirectUris = { "http://localhost:5000/index.html" },
AllowedCorsOrigins = { "http://localhost:5000", "https://localhost:44300" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"MemberApi",
"MemberApi.full",
"MemberApi.limited"
}
}
Instead of using TokenClient, the intention is to use extension methods (on HttpClient) in the future.
The extension method with similar functionality to the TokenClient class is RequestClientCredentialsTokenAsync
Use it as follows:
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://auth.myserver.com:5011");
var tokenResponse = await client.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "MyAppClient",
ClientSecret = "secret",
Scope = "api1"
});
It's confusing, as documentation needs to cover a lot of different situations. It's best you concentrate on the implicit flow, as that's a good candidate for a SPA application.
There is a sample and documentation available from identityServer, take a look at JavaScriptClient.
Next best thing to do is read the specs. That's the source. IdentityServer implements those specifications. In addition you can read the documentation of IdentityServer.
That should help you to understand when and where to use the tokens. It depends on the type of client and the chosen flow. You can find information about that here.
In short the meaning of the tokens:
Access Token: a JWT or reference token, used to access a resource. This token is also used to retrieve information from the UserInfo endpoint.
Identity Token: contains at least the sub claim, in order to identify the user.
Refresh Token: a token to allow a client to obtain a new access token on behalf of the user without requiring interaction with the user (offline access). Only available in certain flows.
Depending on the flow the response can differ.
For documentation about how to access a resource (api) with javascript using the access token please read this. When the access token expires you'll need a new token, without having to ask the user to login again. Since the refresh token isn't supported for the implicit flow you can implement a silent token renew as described here.
About the obsolete code, TokenClient is replaced in favor of HttpClient. Instead of setting up a TokenClient you can now call the extension for the HttpClient. This is a code improvement and doesn't affect the implementation of the flows.
I am trying to combine OpenId authentication and bearer token authentication through the use of IdentityServer.
My project is an Asp.net MVC project, with a web api component. I would like to use OpenId authentication for the standard MVC controllers, and bearer token authentication for the web api component.
My front end is written in Angularjs, and calls web api methods through the $http service to populate the UI.
I am having a problem setting up the authentication workflow.
My thoughts initially were to try to execute the following algorithm:
User requests some web api method from the server through an ajax call ($http)
App detects the user is not authenticated and bearer token is not present in request header, redirects to MVC controller method to retrieve the bearer token.
Since the user is not authenticated, the server will redirect to the IdentityServer default login page.
Once the user is logged in, the server automatically redirects back to the bearer token method to get a bearer token
The generated bearer token is somehow returned to the client.
However my algorithm breaks down at step 3. I am trying to implement step 2 by using an httpinterceptor to inspect outgoing request to determine if a bearer token is present.
app.factory('authInterceptorService', ['$q', '$location', 'localStorageService', function ($q, $location, localStorageService) {
var authInterceptorServiceFactory = {};
var _request = function (config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
else {
$location.path('/Authentication/BearerToken');
}
return config;
}
var _responseError = function (rejection) {
if (rejection.status === 401) {
$location.path('/Authentication/BearerToken');
}
return $q.reject(rejection);
}
authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
return authInterceptorServiceFactory;}]);
If the token is not present, I exit the SPA and redirect to the bearer token MVC method.
This will successfully redirect to the login page, however once I log in, I am returned a bearer token in JSON directly on the page, since I am now outside of the Ajax call.
So my question is
could you please provide me with alternate ideas (and an outline of an implementation) on how to combine these two modes of authentication into one workflow?
Perhaps there is a way to customize IdentityServer to do what I want?
My basic requirement is that an unauthenticated user on my SPA angularjs app is redirected to the default IdentityServer login page, and once logged in, that the initial request be fulfilled.
Thanks in advance!
Just in case you are curious, my IdentityServer setup is as follows. The authentication service is in a separate project from the web application. Each has it's own Startup.cs file.
The MVC Startup.cs file
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);
}
}
};
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);
});
}
}
The IdentityServer Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
var policy = new System.Web.Cors.CorsPolicy
{
AllowAnyOrigin = true,
AllowAnyHeader = true,
AllowAnyMethod = true,
SupportsCredentials = true
};
//policy.ExposedHeaders.Add("location: http://location.com");
policy.Headers.Add("location: testing");
app.UseCors(new CorsOptions
{
PolicyProvider = new CorsPolicyProvider
{
PolicyResolver = context => Task.FromResult(policy)
}
});
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get())
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\Configuration\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
EDIT
I have made some progress in redirecting to the Login page. The issue I was having is that the initial call to the web api was being redirected by IdentityServer with a status code of 302. This meant that I could not get access to the Location header which contains the url I want to redirect to. So instead, I added some owin middleware to check the status code, and if a 302, return a 401 instead (I need to also check that the call is Ajax but have not yet implemented this).
app.Use(async (environment, next) =>
{
await next();
if (environment.Response.StatusCode == 302)
{
environment.Response.StatusCode = 401;
}
});
This then gives me access to the Location header on the client side, and I can redirect, like so:
getPlayerList: function (queryParameters) {
var deferred = $q.defer();
$http.post('api/pitchingstats/GetFilteredPlayers', {
skip: queryParameters.skip,
take: queryParameters.take,
orderby: queryParameters.orderby,
sortdirection: queryParameters.sortdirection,
filter: queryParameters.filter
}).success(function (data, status, headers, config) {
if (status === 401) {
window.location = headers().location;
}
deferred.resolve(data);
}).error(function (data, status, headers, config) {
deferred.reject(status);
});
I know it's a hack, but it's the only way I can make my approach work currently. By all means feel free to suggest other methods.