With the help of this guide i created one IdentityServer4 on localhost:5000
https://identityserver4.readthedocs.io/en/latest/
this the discovery document:
{
"issuer": "http://localhost:5000",
"jwks_uri": "http://localhost:5000/.well-known/openid-configuration/jwks",
"authorization_endpoint": "http://localhost:5000/connect/authorize",
"token_endpoint": "http://localhost:5000/connect/token",
"userinfo_endpoint": "http://localhost:5000/connect/userinfo",
"end_session_endpoint": "http://localhost:5000/connect/endsession",
"check_session_iframe": "http://localhost:5000/connect/checksession",
"revocation_endpoint": "http://localhost:5000/connect/revocation",
"introspection_endpoint": "http://localhost:5000/connect/introspect",
"device_authorization_endpoint": "http://localhost:5000/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"scopes_supported": [
"profile",
"openid",
"swg_entitlements",
"offline_access"
],
"claims_supported": [
"website",
"picture",
"profile",
"preferred_username",
"nickname",
"middle_name",
"given_name",
"family_name",
"name",
"gender",
"birthdate",
"zoneinfo",
"locale",
"updated_at",
"sub"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"implicit",
"password",
"urn:ietf:params:oauth:grant-type:device_code"
],
"response_types_supported": [
"code",
"token",
"id_token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"response_modes_supported": [
"form_post",
"query",
"fragment"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"subject_types_supported": [
"public"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"request_parameter_supported": true
}
i would like to validate the access token outside the IS
based on this article https://devblogs.microsoft.com/aspnet/jwt-validation-and-authorization-in-asp-net-core/ i wrote this code into a standard .net application:
http://localhost:5000/ is the IdentityServer
http://localhost:5001/ is the Api project
this is the code:
void CheckToken()
{
string token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IlRWUFNScTNWMFZibHIyN0VoY1V2U3ciLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE1OTI0ODY3MDksImV4cCI6MTU5MjQ5MDMwOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoic3dnX2VudGl0bGVtZW50cyIsImNsaWVudF9pZCI6Im12YyIsInN1YiI6ImZkYjEiLCJhdXRoX3RpbWUiOjE1OTI0ODY3MDcsImlkcCI6ImxvY2FsIiwicm9sZSI6WyJkYXRhRXZlbnRSZWNvcmRzLmFkbWluIiwiZGF0YUV2ZW50UmVjb3Jkcy51c2VyIl0sInVzZXJuYW1lIjoiZmRiMSIsInVzZXJJZGVudGl0eSI6InZiM3luN2dEMjRCRWxmT1BaUE1qTUM3NVFLWlJWek10ZU9PVDRHdXk5TFk9Iiwic2NvcGUiOlsicHJvZmlsZSIsIm9wZW5pZCIsInN3Z19lbnRpdGxlbWVudHMiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsicHdkIl19.TCi12FLD1oLK9A7sP9aTWqBwrdOm7HiIkCwy0OJHDQsTkKs4kMtKrZcZCOfI3FErgEEvlTAlwT9t5ERKtF1Nvr9343GfcDMNRWY6Z3KGiKgskB983uOENoZZ3Hr72OOEttwK-e3Y01LuudHVNoYaX4zwX8RXTBVGu9NVOhQpksGj8uqljyxzS5ulO3wb73TEX3Z6dAClGrme-zbvc5fN4zvzfWu43fVBIbDcaiqWephWGxyK2iyyeQpMH8Om0OhWKV68vQ5H4yoE8fFWq0LWA4uRvMoAVYL6DOfzWjIF1ZeJWtD0yFxC0h9aspWj9bJVFa4GMtUsF1hkzzs9hKXw8g";
//read secretKey from the table ClientSecrets of the IdentityServer configuration database
const string secretKey = "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=";
var securityKey = new SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(secretKey));
var handler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = "http://localhost:5001/",
ValidIssuer = "http://localhost:5000/",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
LifetimeValidator = LifetimeValidator,
IssuerSigningKey = securityKey
};
System.Security.Principal.IPrincipal userName = handler.ValidateToken(token, validationParameters, out _);
}
this the error in ValidateToken:
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: 'IDX10501: Signature validation failed. Unable to match key:
kid: 'System.String'.
Exceptions caught:
'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.'
This sample code validates a token against your IdentiyServer. Just pass your JWT-token string (that starts with ey) to it and the clientID. Issuer is the URL to your IdentiyServer.
public string ValidateToken(string token, string clientId)
{
try
{
string issuer = openIDSettings.Issuer;
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{issuer}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
var openIdConfig = configurationManager.GetConfigurationAsync(CancellationToken.None).Result;
// Configure the TokenValidationParameters. Assign the SigningKeys which were downloaded from Auth0.
// Also set the Issuer and Audience(s) to validate
//https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs
var validationParameters =
new TokenValidationParameters
{
IssuerSigningKeys = openIdConfig.SigningKeys,
ValidAudiences = new[] { clientId },
ValidIssuer = issuer,
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateTokenReplay = true
};
// Now validate the token. If the token is not valid for any reason, an exception will be thrown by the method
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var user = handler.ValidateToken(token, validationParameters, out validatedToken);
// The ValidateToken method above will return a ClaimsPrincipal. Get the user ID from the NameIdentifier claim
// (The sub claim from the JWT will be translated to the NameIdentifier claim)
return $"Token is validated. User Id {user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value}";
}
catch (Exception exc)
{
return "Invalid token: " + exc.Message;
}
}
Related
i have some issues getting the UserInfo endpoint working using JWT AccessTokens, it works fine with default settings when commenting out the resourceIndicators section.
I can get the access token using PostMan without issues, but when posting on the UserInfo (/me) endpoint the Bearer AccessToken, i got an invalid_token error.
here is my code:
const {Provider} = require('oidc-provider');
let hostname = process.env.HOSTNAME;
if (hostname === undefined) {
hostname = "http://localhost"
}
const port = process.env.PORT || 3000;
if (port !== 80 && port !== 443) {
hostname = hostname + ':' + port
}
const users = [
{
"id": "user1",
"email": "user1#example.com",
"authentication_method_reference": "mfa"
}
]
const clients = [
{
"client_id": "client-1",
"client_secret": "client-1-secret",
"redirect_uris": [
"http://localhost:3000"
]
}
]
async function findAccount (ctx, id) {
// This would ideally be just a check whether the account is still in your storage
let account = users.find(user => {
return user.id === id;
})
if (!account) {
return undefined;
}
return {
accountId: id,
async claims() {
return {
sub: id,
email: account.email,
amr: [account.authentication_method_reference]
};
},
};
}
const configuration = {
clients: clients,
conformIdTokenClaims: false,
features: {
devInteractions: {
enabled: true
},
resourceIndicators: {
defaultResource: (ctx, client, oneOf) => {
return hostname;
},
enabled: true,
getResourceServerInfo: (ctx, resourceIndicator, client) => {
console.log('get resource server info', client);
return ({
audience: resourceIndicator,
scope: 'openid',
accessTokenTTL: 2 * 60 * 60,
accessTokenFormat: 'jwt',
});
},
useGrantedResource: (ctx, model) => { return true; }
}
},
claims: {
openid: [
'sub',
'email',
'amr'
]
},
cookies: {
keys: 'super,secret'.split(',')
},
pkce: {
required: () => false
},
// Used to skip the 'approval' page
async loadExistingGrant(ctx) {
const grantId = (ctx.oidc.result
&& ctx.oidc.result.consent
&& ctx.oidc.result.consent.grantId) || ctx.oidc.session.grantIdFor(ctx.oidc.client.clientId);
if (grantId) {
// keep grant expiry aligned with session expiry
// to prevent consent prompt being requested when grant expires
const grant = await ctx.oidc.provider.Grant.find(grantId);
// this aligns the Grant ttl with that of the current session
// if the same Grant is used for multiple sessions, or is set
// to never expire, you probably do not want this in your code
if (ctx.oidc.account && grant.exp < ctx.oidc.session.exp) {
grant.exp = ctx.oidc.session.exp;
await grant.save();
}
return grant;
} else {
const grant = new ctx.oidc.provider.Grant({
clientId: ctx.oidc.client.clientId,
accountId: ctx.oidc.session.accountId,
});
grant.addOIDCScope('openid');
grant.addResourceScope(hostname, 'openid');
await grant.save();
return grant;
}
},
extraTokenClaims: async (ctx, token) => {
return findAccount(ctx, token.accountId).then(account => {
return account.claims()
})
},
findAccount: findAccount
};
const oidc = new Provider(hostname, configuration);
function handleServerError(ctx, err) {
console.log(err);
}
function handleGrantErrors({headers: {authorization}, oidc: {body, client}}, err) {
console.log(err);
}
function handleAccessToken(token) {
console.log(token);
}
oidc.on('grant.error', handleGrantErrors);
oidc.on('introspection.error', handleGrantErrors);
oidc.on('revocation.error', handleGrantErrors);
oidc.on('server_error', handleServerError);
oidc.on('access_token.issued', handleAccessToken);
oidc.listen(port, () => {
console.log(`oidc-provider listening on port ${port}.`)
})
I tried different configurations without success, the generated JWT AccessToken looks fine to me (see bellow), but i'm unable to query the UserInfo endpoint with it.
{
"sub": "user1",
"email": "user1#example.com",
"amr": [
"mfa"
],
"jti": "-7gURc8Y1SXqOXhWR691i",
"iat": 1668777371,
"exp": 1668784571,
"scope": "openid",
"client_id": "client-1",
"iss": "http://localhost:3000",
"aud": "http://localhost:3000"
}
Thanks in advance.
As per the module documentation's userinfo feature.
Its use requires an opaque Access Token with at least openid scope that's without a Resource Server audience.
In essence, this implementation's userinfo endpoint will not work with JWT Access Tokens that are issued for a particular resource server. This is because the userinfo endpoint is a resource for the client and if it was callable with an access token that was sent to a resource server, that resource server can turn around and query userinfo which is not the intended use of the userinfo endpoint.
In cases when JWT Access Tokens are issued the client will get all scope requested userinfo claims in the ID Token it receives, removing the need to call userinfo.
In spring boot, adding keycloak adaptor and "keycloak.policy-enforcer-config.claimInformationPointConfig.claims[claim-from-uri]={ request.uri }" to application.properties
file, I am able to receive "claim-from-uri" in keycloak javascript policy. But using similar settings in ktor does not work.
I have added "policy-enforcer" to keyclaok.json but "claim-from-uri" property is always null in javascript policy in keycloak.
// js policy in keycloak
var context = $evaluation.getContext();
var attributes = context.getAttributes();
var realm = $evaluation.getRealm();
var httpUri = attributes.getValue('http.uri');
var claimFromUri = attributes.getValue('claim-from-uri');
My usecase is to get the claim from the URI and then use it to get the policy from the keycloak server.
Below is my keycloak.json file.
{
"realm": "test-realm",
"auth-server-url": "https://localhost:8080/auth",
"ssl-required": "none",
"resource": "api-resource",
"public-client": true,
"policy-enforcer": {
"enforcement-mode": "ENFORCING",
"paths": [
{
"path": "/api/*",
"claim-information-point": {
"claims": {
"claim-from-uri": "{request.uri}"
}
},
"methods": [
{
"method": "GET",
"scopes": ["get", "GET"]
},
{
"method": "POST",
"scopes": ["post", "POST"]
}
]
}
]
}
}
val keycloakProvider = OAuthServerSettings.OAuth2ServerSettings(
name = "keycloak",
authorizeUrl = "https://localhost:8082/auth/realms/test-realm/protocol/openid-connect/auth",
accessTokenUrl = "https://localhost:8082/auth/realms/test-realm/protocol/openid-connect/token",
clientId = "test-realm-backend",
clientSecret = "client-secret",
accessTokenRequiresBasicAuth = false,
requestMethod = HttpMethod.Post,
)
//application setup
install(Authentication) {
oauth("keycloak") {
client = HttpClient(Apache)
providerLookup = { keycloakProvider }
urlProvider = { "http://localhost:8080/callback" }
}
}
// routing
authenticate("keycloak") {
get("/api/{name}") {
val principal: OAuthAccessTokenResponse.OAuth2? = call.authentication.principal()
call.sessions.set(UserSession("Bearer $principal?.accessToken.toString()"))
val name = call.parameters["name"] ?: "name missing in parameter"
val user = User(name)
call.respond(user)
}
}
this is my middleware in backend
const jwt = require('jsonwebtoken');
const User = require('../models/user');
module.exports = (req, res, next) => {
const auth = req.get('Authorization');
if (!auth) {
return res.status(401).json({
"response": {
"message": 'Not authorized user',
}
})
}
const token = auth.split(' ')[1];
let decode;
try {
decode = jwt.verify(token, process.env.JWT_SECRET_KEY)
} catch (err) {
return res.status(401).json({
"response": {
"message": 'Invalid token access',
}
})
}
if (!decode) {
return res.status(500).json({
"response": {
"message": 'Error token access',
}
})
}
let userId = decode.id;
User.findById(userId)
.then(user => {
req.user = user;
next();
}).catch(err => {
console.log(err);
res.status(401).json({
"response": {
"message": `Error on authorization`,
}
})
})
}
I need integration of auto refresh token on each call when expire, and how can do for resave in localstorage in my client side?
Have my front-end developed with vue and vuex. Use this localStorage.setItem('access_token', token); code for save in localstorage.
Below is the logic that I did in my own web app
When user logs in you need to create access and refresh token
After you receive both tokens keep them in localStorage or wherever is safe
You need to create a refreshToken route(/refresh-token) to call when your access token expired
Define a middleware to check tokens and use it in secured routes
When you get an 401 error call /refresh-token route to refresh the token
config file
{
"secret": "secret",
"refreshTokenSecret": "refresh-secret",
"port": 8080,
"tokenLife": 900,
"refreshTokenLife": 86400
}
login route(app.js)
router.get('/login',(req, res) => {
const postData = req.body;
const user = {
"email": postData.email,
"name": postData.name
}
const token = jwt.sign(user, config.secret, { expiresIn: config.tokenLife})
const refreshToken = jwt.sign(user, config.refreshTokenSecret, { expiresIn:
config.refreshTokenLife})
const response = {
"status": "Logged in",
"token": token,
"refreshToken": refreshToken,
}
tokenList[refreshToken] = response
res.status(200).json(response);
}
refresh token route(app.js)
router.post('/refresh-token', (req,res) => {
const postData = req.body
if((postData.refreshToken) && (postData.refreshToken in tokenList)) {
const user = {
"email": postData.email,
"name": postData.name
}
const token = jwt.sign(user, config.secret, { expiresIn: config.tokenLife})
const response = {
"token": token,
}
// update the token in the list
tokenList[postData.refreshToken].token = token
res.status(200).json(response);
} else {
res.status(404).send('Invalid request')
}
})
tokenMiddleware.js
const jwt = require('jsonwebtoken')
const config = require('./config')
module.exports = (req,res,next) => {
const token = req.body.token || req.query.token || req.headers['x-access-token']
if (token) {
jwt.verify(token, config.secret, function(err, decoded) {
if (err) {
return res.status(401).json({"error": true, "message": 'Unauthorized access.' });
}
req.decoded = decoded;
next();
});
} else {
return res.status(403).send({
"error": true,
"message": 'No token provided.'
});
}
}
use middleware to protect routes
router.get('/secured-route', tokenMiddleware, async (req, res) => {
res.send("secured");
});
I have 2 sites (www.dogsite.com and www.catsite.com). They are pointed to login.identityserverdemo.com as my authority server. This allows the SSO to work between the two sites. Now if I add two more bindings to my login.identityserverdemo.com site called login.dogsite.com and login.catsite.com, I will lose the SSO. I have tried adding the www.dogsite.com and www.catsite.com to the client's redirectUrls with no improvement.
Here is my client config:
new Client
{
ClientId = "mvc-dog",
ClientName = "MVC Client For Dog Site",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("woofwoof123".Sha256())
},
Claims = new List<Claim>
{ },
RedirectUris = { "http://www.catsite.com/signin-oidc", "http://www.dogsite.com/signin-oidc" },
PostLogoutRedirectUris = { "http://www.catsite.com/signout-callback-oidc","http://www.dogsite.com/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
},
new Client
{
ClientId = "mvc-cat",
ClientName = "MVC Client For Cat Site",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("MeowMeow456".Sha256())
},
Claims = new List<Claim>
{},
RedirectUris = { "http://www.catsite.com/signin-oidc", "http://www.dogsite.com/signin-oidc" },
PostLogoutRedirectUris = { "http://www.catsite.com/signout-callback-oidc","http://www.dogsite.com/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
}
Please help??
You lose SSO because login.dogsite.com and login.catsite.com cannot get access to the same idSrv cookies.
Check out Cross domain cookie option. It's for ID3 but is equally applicable to ID4.
I'm trying out a self-hosted IdentityServer3 solution and have come across an issue that I haven't found an answer to.
This is my IdentityServer setup:
var factory = new IdentityServerServiceFactory();
factory.UseInMemoryClients(Config.GetClients())
.UseInMemoryScopes(Config.GetScopes());
factory.UserService = new Registration<IUserService>(resolver => new LocalRegistrationUserService());
var options = new IdentityServerOptions
{
SiteName = "Demo IdP",
SigningCertificate = Certificate.Get(),
Factory = factory,
RequireSsl = Convert.ToBoolean(ConfigurationManager.AppSettings["RequireSsl"]),
};
app.UseIdentityServer(options);
Scope(s) and client(s):
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "resourceowner.client",
ClientSecrets =
{
new Secret("3FE8FB45-627A-4C44-BBE3-63281C6CA910".Sha256())
},
AllowedScopes = { "demo", "openid", "profile" },
Flow = Flows.ResourceOwner,
}
};
}
public static IEnumerable<Scope> GetScopes()
{
return new List<Scope>
{
new Scope
{
Name = "demo",
DisplayName = "Demo",
},
StandardScopes.OpenId,
StandardScopes.Profile,
};
}
And in LocalRegistrationUserService.AuthenticateLocalAsync(LocalAuthenticationContext context) I've got:
var loginResult = new AccountManagementService().Login(context.UserName, context.Password);
if (loginResult.LoginOk)
{
context.AuthenticateResult = new AuthenticateResult(loginResult.Subject, loginResult.UserName);
}
else
{
....
}
My understanding of the following paragraph from the documentation
*To fully log the user in the authentication API must produce a subject and a name that represent the user. The subject is the user service’s unique identifier for the user and the name is a display name for the user that will be displayed in the user interface.*
is that the subject and username will be present in the token returned by IdentityServer. However, when I decode the token this is what I get:
Access Token (decoded):
{
"typ": "JWT",
"alg": "RS256",
"x5t": "a3rMUgMFv9tPclLa6yF3zAkfquE",
"kid": "a3rMUgMFv9tPclLa6yF3zAkfquE"
}
{
"iss": "http://localhost:44333/core",
"aud": "http://localhost:44333/core/resources",
"exp": 1478524845,
"nbf": 1478521245,
"client_id": "resourceowner.client",
"scope": [
"demo",
"openid",
"profile"
],
"sub": "6ace8b2e-ce20-41e9-8d4e-382168e4ce05",
"auth_time": 1478521245,
"idp": "idsrv",
"amr": [
"password"
]
}
As is evident, no name claim is present. I've tried adding the claim explicitly when instantiating AuthenticateResult, but to no avail. I'm clearly missing something here, but can't for the best of it figure out what I'm doing wrong so tips, pointers and/or a helpful example would be much appreciated.
TIA