AuthenticateResult: Why are some claims missing? - identityserver3

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

Related

node-oidc-provider 7.12.0 (JWT, authorization_code) invalid_token error on UserInfo endpoint (/me)

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.

What is the proper way to register policy-enforcer configuration in ktor

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)
}
}

How to validate a jwt token released from IdentityServer4

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;
}
}

IdentityServer 4 with Multiple Bindings

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.

How to get all versions of an object in Google cloud storage bucket?

In a web page hosted in Google cloud storage, I will like to show revision history, which require listing all versions of the object.
Sending GET request to the bucket with ?versions parameter return list versions all objects. Is there any way to list all versions of a single object, as in gsutil ls -la, in javascript?
There is not. The closest you can do is to use versions=true and prefix=YOUR_OBJECT_NAME.
GCS will respond with a listing of objects beginning with all of the versions of your object and continuing on to any other objects that begin with YOUR_OBJECT_NAME. You'll have to check those items to see when the listing runs out of versions of your object and moves on to other objects.
If it so happens that only one object begins with YOUR_OBJECT_NAME (for example, your object is "foo.txt" and there are no files named, say, "foo.txt.backup", you will get exactly the files you want. You probably don't want to rely on this as a general practice, though.
Brondon's answer work with XML, but not with gapi client.
/**
* Get versions meta data of the object.
* #return {goog.async.Deferred} return history of the object.
*/
mbi.data.Object.prototype.history = function() {
var df = new goog.async.Deferred();
var use_gapi = true;
var name = this.getName();
if (use_gapi) {
// GAPI does not return result for versions request.
var params = {
'bucket': this.getBucketName(),
'versions': true,
'prefix': name
};
// console.log(params);
var req = gapi.client.rpcRequest('storage.buckets.get',
mbi.app.base.GAPI_STORAGE_VERSION, params);
req.execute(function(json, row) {
if (json) {
df.callback(json);
} else {
df.errback();
throw new Error(row);
}
});
} else {
var xm = mbi.data.Object.getXhr();
var uri = new goog.Uri(this.getUrl());
uri.setParameterValue('versions', 'true');
uri.setParameterValue('max-keys', '25');
uri.setParameterValue('prefix', name);
var url = uri.setPath('').setFragment('').toString();
xm.send(url, url, 'GET', null, {}, 1, function(e) {
var xhr = /** #type {goog.net.XhrIo} */ (e.target);
if (xhr.isSuccess()) {
var xml = xhr.getResponseXml();
// console.log(xml);
var json = mbi.utils.xml.xml2json(xml);
var items = json['ListBucketResult']['Version'];
var versions = goog.isArray(items) ? items : items ? [items] : [];
versions = versions.filter(function(x) {
return x['Key'] == name;
});
df.callback(versions);
} else {
df.errback(xhr.getStatus() + ' ' + xhr.getResponseText());
}
});
}
return df;
};
GAPI return as follow without version meta:
[
{
"id": "gapiRpc",
"result": {
"kind": "storage#bucket",
"id": "mbiwiki-test",
"name": "mbiwiki-test",
"timeCreated": "2013-08-20T01:18:46.957Z",
"metageneration": "9",
"owner": {
"entity": "group-00b4903a97262a358b97b95b39df60893ece79605b60280ad389c889abf70645",
"entityId": "00b4903a97262a358b97b95b39df60893ece79605b60280ad389c889abf70645"
},
"location": "US",
"website": {
"mainPageSuffix": "index.html",
"notFoundPage": "error404.html"
},
"versioning": {
"enabled": true
},
"cors": [
{
"origin": [
"http://static.mechanobio.info",
"http://mbinfo-backend.appspot.com",
"https://mbinfo-backend.appspot.com",
"http://localhost",
"chrome-extension://pbcpfkkhmlbicomenogobbagaaenlnpd",
"chrome-extension://mhigmmbegkpdlhjaphlffclbgkgelnbe",
"chrome-extension://jhmklemcneaienackijjhdikoicmoepp"
],
"method": [
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"PATCH"
],
"responseHeader": [
"content-type",
"Authorization",
"Cache-Control",
"x-goog-meta-reviewer"
]
}
],
"storageClass": "STANDARD",
"etag": "CAk="
}
}
]