The scenario is my client's possibility to grab data about my users. There is the code for config server:
Startup.cs
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryIdentityResources(ApiConfiguration.GetIdentityResources()) .AddInMemoryApiResources(ApiConfiguration.GetAllResources())
.AddInMemoryClients(ApiConfiguration.GetAllClients())
.AddTestUsers(ApiConfiguration.GetUsers())
ApiConfiguration
public static IEnumerable<ApiResource> GetAllResources()
{
yield return new ApiResource
{
Name = "customAPI",
Description = "Custom API Access",
UserClaims = new List<string> { "role" },
ApiSecrets = new List<Secret> { new Secret("scopeSecret".Sha256()) },
Scopes = new List<Scope> {
new Scope("customAPI"),
}
};
}
public static IEnumerable<Client> GetAllClients()
{
yield return new Client
{
ClientId = "oauthClient",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> {
new Secret("Password".Sha256())},
AllowedScopes = new List<string> { "customAPI" }
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "psw",
Claims = new List<Claim> {
new Claim(ClaimTypes.Email, "1#1.com"),
new Claim(ClaimTypes.Role, "admin")
}
}
};
}
With this request:
POST /connect/token
Headers:
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=client_credentials&scope=customAPI&client_id=oauthClient&client_secret=Password
the client got the access token. So my question is how can I use the token? What I need http-request to grab data about bob (test user)?
And there is another related question: how to config api to my client could access token for specific user only?
Looking at the configuration above, the client you have setup is using the ClientCredentials grant, this is an OAuth grant type. If you wish to add identity you should look to use an OpenID Connect grant type, this will provide you with identity and tokens specific to the user you authenticate as.
More information on using OpenID Connect with Identity Server can be found in the docs at http://docs.identityserver.io/en/release/quickstarts/3_interactive_login.html
If you work through the following quickstart tutorials, you will see it come together.
Setup and Overview
Protecting an API using Client Credentials
Protecting an API using Passwords
Adding User Authentication with OpenID Connect
Switching to Hybrid Flow and adding API Access back
(You can skip the one on External Authentication.)
https://identityserver4.readthedocs.io/en/release/quickstarts/0_overview.html
Related
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 new to Identity Server 4, and I am struggling to obtain the users identities. At the moment, I am displaying the Claims via an API that Identity Server is protecting as so:
namespace API01.Controllers
{
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
// GET identity
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
}
The problem with this is that email resource is just showing up as the value email when I decode the jwt.
"scope": [
"email",
"openid",
"api1"
],
I have been experimenting with User.Identities but so far I cannot get the information I need from my AllowedScopes {"email", "openid", "api1"}.
Basically, I want to obtain the value which in my case is test#test.com. I am not worried about returning a JsonResult, just a string would suffice for now, if its going to be difficult.
If you want to include the email claim in your id token , you can add the IdentityResources.Email() in IdentityResource of IDS4 :
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
Also set AlwaysIncludeUserClaimsInIdToken to true in client config :
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
....
....
AlwaysIncludeUserClaimsInIdToken = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1",
IdentityServerConstants.StandardScopes.Email,
},
AllowOfflineAccess = true
},
You can start from Identity Server4 code samples .
If you want to find the scopes in jwt token , id token won't include the scopes claim , but access token includes since api should validate that .
Scope array indicates what is allowed to access.
You probably want to see email claim in the token.
For that you need to implement IProfilrService and add all the claims from Subject to IssuedClaims.
I am trying to setup an IdentityServer 3 web application not hardware, this is a software development related question. I am trying to learn how to use the technology and produce JWT token's that my api can consume. The problem is I cannot for the life of me find where to set the token expiration. It always produces a 401 after about an hour. Ideally for testing purposes I would like to extend this to a very long time so I do not have to keep copy and pasting my JWT token into fiddler thus dramatically slowing down my development and learning process.
My Client
new Client
{
ClientId = "scheduling"
,ClientSecrets = new List<Secret>
{
new Secret("65A6A6C3-A764-41D9-9D10-FC09E0DBB046".Sha256())
},
ClientName = "Patient Scheduling",
Flow = Flows.ResourceOwner,
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.OfflineAccess,
"read",
"adprofile",
"scheduling"
},
Enabled = true
}
My Scope
new Scope
{
Name = "scheduling",
Claims = new List<ScopeClaim>
{
new ScopeClaim(Constants.ClaimTypes.Role,true),
new ScopeClaim("scheduling_id",true),
new ScopeClaim("expires_at",true) //I have tried "expires_in" and [Constants.ClaimTypes.Expiration] also with no luck
}
}
Method used for client specific claims:
private IEnumerable<Claim> GetClaimByClientId(string client_id)
{
List<Claim> claims = new List<Claim>();
switch(client_id.ToLower())
{
case "scheduling":
claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Role,"administrator"));
claims.Add(new Claim("scheduling_id", "2"));
//claims.Add(new Claim("expires_in", "2082758400")); //01/01/2036
//claims.Add(new Claim(Constants.ClaimTypes.Expiration, "2082758400")); //01/01/2036
claims.Add(new Claim("expires_at", "2082758400")); //01/01/2036
break;
default:
throw new Exception("Client not found with provided client id.");
}
return claims;
}
Code actually validating Credentials:
if (ActiveDirectoryHelper.ValidateCredentials(context.UserName, context.Password, adName))
{
List<Claim> lstClaims = new List<Claim>
{
new Claim("obj_id",user.UserID.ToUpper()),
new Claim(Constants.ClaimTypes.Email, string.IsNullOrEmpty(user.Email) ? string.Empty : user.Email.ToLower()),
new Claim(Constants.ClaimTypes.GivenName,user.FirstName),
new Claim(Constants.ClaimTypes.FamilyName,user.LastName),
new Claim("EmployeeNumber",user.EmployeeNumber),
};
lstClaims.AddRange(GetClaimByClientId("scheduling"));
context.AuthenticateResult = new AuthenticateResult(user.UserID,user.Username, lstClaims);
}
else
{
context.AuthenticateResult = new AuthenticateResult("Invalid Login.");
}
Access Token lifetime (I assume this is what you mean by JWT token) can be set for a client application using the Client property AccessTokenLifetime.
By default this is set to 3600 seconds (1 hour).
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.
We have developed a set of Web APIs (REST) which are protected by an Authorization server. The Authorization server has issued the client id and client secret. These can be used to obtain an access token. A valid token can be used on subsequent calls to resource servers (REST APIs).
I want to write an web based (Asp.net MVC 5) client that will consume the APIs. Is there a nuget package I can download that will help me to implement the client OAuth2 flow? Can anyone direct me to a good example on client implementation of OAuth2 flow (written in asp.net MVC)?
Update
I was able to get access token using the code block below, but what I want is a "client credentials" oauth 2 flow where I don't have to enter login and passwords. The code I have now is:
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)
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),
ClientId = ConfigurationManager.AppSettings["AuthServer:ClientId"],
ClientSecret = ConfigurationManager.AppSettings["AuthServer:ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["AuthServer:RedirectUrl"],
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://identityserver.com/oauth2/authorize",
TokenEndpoint = "https://identityserver.com/oauth2/token"
},
//ResponseType = "client_credentials", // Doesn't work
ResponseType = "token",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = notification =>
{
if (string.Equals(notification.ProtocolMessage.Error, "access_denied", StringComparison.Ordinal))
{
notification.HandleResponse();
notification.Response.Redirect("/");
}
return Task.FromResult<object>(null);
},
AuthorizationCodeReceived = async notification =>
{
using (var client = new HttpClient())
{
//var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
String tokenEndPoint = "https://identityserver.com/oauth2/token";
//var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint);
var request = new HttpRequestMessage(HttpMethod.Post, 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.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)));
}
}
}
});
}
}
To support the client credentials grant type, your best option is probably to directly use HttpClient:
var request = new HttpRequestMessage(HttpMethod.Post, "http://server.com/token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "client_id", "your client_id" },
{ "client_secret", "your client_secret" },
{ "grant_type", "client_credentials" }
});
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
var token = payload.Value<string>("access_token");
For interactive flows (like the authorization code flow), there are two better approaches:
If your authorization server supports OpenID Connect (which is based on OAuth2), you can simply use the OpenID Connect middleware for OWIN/Katana 3 developed by Microsoft: https://www.nuget.org/packages/Microsoft.Owin.Security.OpenIdConnect/
If OpenID Connect is not supported by your authorization server, one option is to create your own OAuth2 client middleware. You can take a look at the last part of this SO answer for more information: Registering Web API 2 external logins from multiple API clients with OWIN Identity