I have a .NET Core 6.0 project that I implemented IdentityServer4.
When I use ResourceOwnerPassword or ClientCredentials grant flow, there is no problem...
But when I use Hybrid grant flow, I don't know how to do that...
This is how I implement ResourceOwnerPassword grant type for my project..
IdentityServer project Client (Resource Owner Password Grant Type)
new Client
{
ClientName="WebUI First For User",
ClientId="WebUI_First_User",
AllowOfflineAccess=true,
ClientSecrets= {new Secret("first_user".Sha256())},
AllowedGrantTypes= GrantTypes.ResourceOwnerPassword,
AllowedScopes={ "catalog_fullpermission",
"webui_user_first",
"gateway_fullpermission",
"roles",
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
IdentityServerConstants.LocalApi.ScopeName
},
AccessTokenLifetime=1*60*60,
RefreshTokenExpiration=TokenExpiration.Absolute,
AbsoluteRefreshTokenLifetime= (int) (DateTime.Now.AddDays(60)- DateTime.Now).TotalSeconds,
RefreshTokenUsage= TokenUsage.ReUse
}
IdentityServer project Client (Client Credentials Grant Type)
new Client
{
ClientName="WebUI First",
ClientId="WebUI_First",
ClientSecrets= {new Secret("first".Sha256())},
AllowedGrantTypes= GrantTypes.ClientCredentials,
AllowedScopes={ "catalog_fullpermission",
"webui_first",
"gateway_fullpermission",
IdentityServerConstants.LocalApi.ScopeName,
IdentityServerConstants.StandardScopes.OpenId
}
}
SignIn Method
var disco = await _httpClient.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = _serviceApiSettings.IdentityBaseUri,
Policy = new DiscoveryPolicy { RequireHttps = false }
});
if (disco.IsError)
{
throw disco.Exception;
}
var passwordTokenRequest = new PasswordTokenRequest
{
ClientId = _clientSettings.WebClientForUser.ClientId,
ClientSecret = _clientSettings.WebClientForUser.ClientSecret,
UserName = signinInput.Email,
Password = signinInput.Password,
Address = disco.TokenEndpoint
};
//This is how I get token with ResourceOwnerPassword
var token = await _httpClient.RequestPasswordTokenAsync(passwordTokenRequest);
...
...
IdentityServer project Client (Hybrid Grant Type)
new Client
{
ClientName = "MVC Client",
ClientId = "mvc-client",
AllowedGrantTypes = GrantTypes.Hybrid,
RedirectUris = new List<string>{ "https://localhost:5001/signin-oidc" },
RequirePkce = false,
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile },
ClientSecrets = { new Secret("MVCSecret".Sha512()) }
}
Here I use PasswordTokenRequest class and RequestPasswordTokenAsync method which comes as an extension method...
But I don't know which method I should use for Hybrid grant flow and how to...
Is there anyone having the same issue and solved the problem?
..
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 want add some information on tokenResponse.AccessToken(PhoneNumber, Address,...) when use RequestPasswordTokenAsync.
Is there any way to do that?
var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "ro.client.30",
ClientSecret = "secret",
UserName = "aaa",
Password = "bbb",
Scope = "api1",
});
if you are using InMemory ApiResources, change you Config in this way :
var api1 = new ApiResource("api1", "My API");
api1.UserClaims.Add("PhoneNumber");
new List<ApiResource>
{
api1
};
when someone request for a token which contains api1 scope, specefied UserClaims in api1's ApiResource will be added to it's token
I'm new to IdentityServer. We have a requirement for the application to allow access to multiple web APIs. As of now authentication is done locally with a database and there is alternate way to get authenticated by Azure AD.
I want my dashboard app to authenticate using IdentityServer3 (which is working fine as of now) and alternatively using external provider (Azure AD in this case).
However I keep getting
There is an error determining which application you are signing into. Return to the application and try again
The configuration for server, I'm using CustomViewService found at here
I'm adding Azure AD to the list of external providers:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "xxxxxx-xxx-xxx-xx-04ec8dbxxxx",
Authority = "https://login.windows.net/[domain name]",
RedirectUri = "https://localhost:44333/core",
PostLogoutRedirectUri = "http://localhost:36336/",
AuthenticationType = "Azure AD",
Caption = "Azure AD",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
MessageReceived = m =>
{
var split = m.ProtocolMessage.AccessToken;
return Task.FromResult(0);
},
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.DomainHint = "[domain name here]";
return Task.FromResult(0);
}
}
});
I'm presented with Azure AD log in screen, after which application is diverted back to IdentityServ3 at https://localhost:44333/core/callback
My client is at http://localhost:36336/
Client configuration is:
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "mvc.owin.hybrid.2",
Authority = "https://localhost:44333/core",
RedirectUri = "http://localhost:36336/",
PostLogoutRedirectUri = "http://localhost:36336/",
ResponseType = "code id_token",
Scope = "openid profile read write offline_access",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
"https://localhost:44333/core/connect/token",
"mvc.owin.hybrid.2",
"secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
tokenResponse.AccessToken);
var userInfoResponse = await userInfoClient.GetAsync();
// create new identity
var id = new ClaimsIdentity(n.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", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
}
Managed to work it out.
I forgot to implement AspNetIdentityUserService specially the method AuthenticateExternalAsync
Once I mapped the user and claims from Azure to the local user it worked.
Furthermore, if you need to hide local login screen and want your client to directly go to Azure AD login screen make sure to set EnableLocalLogin to false in Client properties
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
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
});