From 9fb9501e1ffebde7c5335702c33e733a712a5806 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 8 Oct 2020 23:47:42 +0300 Subject: [PATCH] Profile service added (#51) --- README.md | 24 ++- e2e/config/api-resources.json | 3 +- e2e/config/api-scopes.json | 3 +- e2e/config/clients-configuration.json | 26 ++- e2e/config/user-configuration.json | 50 +++++ .../authorization-endpoint.e2e-spec.ts.snap | 186 ++++++++++++++++-- .../__snapshots__/token-endpoint.spec.ts.snap | 89 ++++++++- .../userinfo-endpoint.e2e-spec.ts.snap | 140 ++++++++++++- e2e/tests/authorization-endpoint.e2e-spec.ts | 4 +- e2e/tests/token-endpoint.spec.ts | 41 +++- e2e/tests/userinfo-endpoint.e2e-spec.ts | 7 +- src/Config.cs | 2 +- src/ConfigurableProfileService.cs | 40 ++++ src/Startup.cs | 3 +- 14 files changed, 577 insertions(+), 41 deletions(-) create mode 100644 src/ConfigurableProfileService.cs diff --git a/README.md b/README.md index b0a07ca..b59bff8 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,29 @@ This is the sample of using the server in `docker-compose` configuration: { "SubjectId":"1", "Username":"User1", - "Password":"pwd" + "Password":"pwd", + "Claims": [ + { + "Type": "name", + "Value": "Sam Tailor" + }, + { + "Type": "email", + "Value": "sam.tailor@gmail.com" + }, + { + "Type": "some-api-resource-claim", + "Value": "Sam's Api Resource Custom Claim" + }, + { + "Type": "some-api-scope-claim", + "Value": "Sam's Api Scope Custom Claim" + }, + { + "Type": "some-identity-resource-claim", + "Value": "Sam's Identity Resource Custom Claim" + } + ] } ] CLIENTS_CONFIGURATION_PATH: /tmp/config/clients-config.json diff --git a/e2e/config/api-resources.json b/e2e/config/api-resources.json index 9793f6c..dcf5a51 100644 --- a/e2e/config/api-resources.json +++ b/e2e/config/api-resources.json @@ -2,6 +2,7 @@ { "Name": "some-app", "Scopes": ["some-app-scope-1", "some-app-scope-2"], - "ApiSecrets": ["some-app-secret-1"] + "ApiSecrets": ["some-app-secret-1"], + "UserClaims": ["some-app-user-custom-claim"] } ] diff --git a/e2e/config/api-scopes.json b/e2e/config/api-scopes.json index 993351e..ad2dd77 100644 --- a/e2e/config/api-scopes.json +++ b/e2e/config/api-scopes.json @@ -1,6 +1,7 @@ [ { - "Name": "some-app-scope-1" + "Name": "some-app-scope-1", + "UserClaims": ["some-app-scope-1-custom-user-claim"] }, { "Name": "some-app-scope-2" diff --git a/e2e/config/clients-configuration.json b/e2e/config/clients-configuration.json index 5380cad..bf87e00 100644 --- a/e2e/config/clients-configuration.json +++ b/e2e/config/clients-configuration.json @@ -5,13 +5,13 @@ "AllowedGrantTypes": ["implicit"], "AllowAccessTokensViaBrowser": true, "RedirectUris": ["https://*.google.com"], - "AllowedScopes": ["openid", "profile", "email", "some-custom-identity"], + "AllowedScopes": ["openid", "profile", "email", "some-custom-identity", "some-app-scope-1"], "IdentityTokenLifetime": 3600, "AccessTokenLifetime": 3600 }, { - "ClientId": "client-credentials-mock-client-id", - "ClientSecrets": ["client-credentials-mock-client-secret"], + "ClientId": "client-credentials-flow-client-id", + "ClientSecrets": ["client-credentials-flow-client-secret"], "Description": "Client for client credentials flow", "AllowedGrantTypes": ["client_credentials"], "AllowedScopes": ["some-app-scope-1"], @@ -28,6 +28,26 @@ } ] }, + { + "ClientId": "password-flow-client-id", + "ClientSecrets": ["password-flow-client-secret"], + "Description": "Client for password flow", + "AllowedGrantTypes": ["password"], + "AllowedScopes": ["openid", "profile", "email", "some-custom-identity", "some-app-scope-1"], + "ClientClaimsPrefix": "", + "Claims": [ + { + "Type": "string_claim", + "Value": "string_claim_value" + }, + { + "Type": "json_claim", + "Value": "['value1', 'value2']", + "ValueType": "json" + } + ], + "RequireClientSecret": false + }, { "ClientId": "introspect-client-id", "ClientSecrets": ["introspect-client-secret"], diff --git a/e2e/config/user-configuration.json b/e2e/config/user-configuration.json index 7f62a31..3801020 100644 --- a/e2e/config/user-configuration.json +++ b/e2e/config/user-configuration.json @@ -41,5 +41,55 @@ "Value": "Jack's Custom User Claim" } ] + }, + { + "SubjectId": "user_with_custom_api_resource_claims", + "Username": "user_with_custom_api_resource_claims", + "Password": "pwd", + "Claims": [ + { + "Type": "name", + "Value": "Sam Tailor" + }, + { + "Type": "email", + "Value": "sam.tailor@gmail.com" + }, + { + "Type": "some-app-user-custom-claim", + "Value": "Sam's Custom User Claim" + }, + { + "Type": "some-app-scope-1-custom-user-claim", + "Value": "Sam's Scope Custom User Claim" + } + ] + }, + { + "SubjectId": "user_with_all_claim_types", + "Username": "user_with_all_claim_types", + "Password": "pwd", + "Claims": [ + { + "Type": "name", + "Value": "Oliver Hunter" + }, + { + "Type": "email", + "Value": "oliver.hunter@gmail.com" + }, + { + "Type": "some-app-user-custom-claim", + "Value": "Oliver's Custom User Claim" + }, + { + "Type": "some-app-scope-1-custom-user-claim", + "Value": "Oliver's Scope Custom User Claim" + }, + { + "Type": "some-custom-identity-user-claim", + "Value": "Oliver's Custom User Claim" + } + ] } ] diff --git a/e2e/tests/__snapshots__/authorization-endpoint.e2e-spec.ts.snap b/e2e/tests/__snapshots__/authorization-endpoint.e2e-spec.ts.snap index 3a54624..fe83531 100644 --- a/e2e/tests/__snapshots__/authorization-endpoint.e2e-spec.ts.snap +++ b/e2e/tests/__snapshots__/authorization-endpoint.e2e-spec.ts.snap @@ -17,6 +17,40 @@ exports[`Authorization Endpoint Authorization Code Flow (with PKCE): simple_user } `; +exports[`Authorization Endpoint Authorization Code Flow (with PKCE): user_with_all_claim_types access token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "authorization-code-with-pkce-client-id", + "sub": "user_with_all_claim_types", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`Authorization Endpoint Authorization Code Flow (with PKCE): user_with_custom_api_resource_claims access token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "authorization-code-with-pkce-client-id", + "sub": "user_with_custom_api_resource_claims", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + exports[`Authorization Endpoint Authorization Code Flow (with PKCE): user_with_custom_identity_claims access token 1`] = ` { "alg": "RS256", @@ -68,6 +102,40 @@ exports[`Authorization Endpoint Authorization Code Flow: simple_user access toke } `; +exports[`Authorization Endpoint Authorization Code Flow: user_with_all_claim_types access token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "authorization-code-client-id", + "sub": "user_with_all_claim_types", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`Authorization Endpoint Authorization Code Flow: user_with_custom_api_resource_claims access token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "authorization-code-client-id", + "sub": "user_with_custom_api_resource_claims", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + exports[`Authorization Endpoint Authorization Code Flow: user_with_custom_identity_claims access token 1`] = ` { "alg": "RS256", @@ -102,23 +170,27 @@ exports[`Authorization Endpoint Authorization Code Flow: user_with_standard_clai } `; -exports[`Authorization Endpoint Implicit Flow 1`] = ` +exports[`Authorization Endpoint Implicit Flow: simple_user access token 1`] = ` { "alg": "RS256", "typ": "JWT", "iss": "http://localhost:8080", - "aud": "implicit-flow-client-id", - "nonce": "xyz", - "s_hash": "ungWv48Bz-pBQUDeXa4iIw", + "aud": "some-app", + "client_id": "implicit-flow-client-id", "sub": "simple_user", "idp": "local", + "scope": [ + "openid", + "some-custom-identity", + "some-app-scope-1" + ], "amr": [ "pwd" ] } `; -exports[`Authorization Endpoint Implicit Flow 2`] = ` +exports[`Authorization Endpoint Implicit Flow: simple_user id token 1`] = ` { "alg": "RS256", "typ": "JWT", @@ -126,7 +198,7 @@ exports[`Authorization Endpoint Implicit Flow 2`] = ` "aud": "implicit-flow-client-id", "nonce": "xyz", "s_hash": "ungWv48Bz-pBQUDeXa4iIw", - "sub": "user_with_custom_identity_claims", + "sub": "simple_user", "idp": "local", "amr": [ "pwd" @@ -134,7 +206,31 @@ exports[`Authorization Endpoint Implicit Flow 2`] = ` } `; -exports[`Authorization Endpoint Implicit Flow 3`] = ` +exports[`Authorization Endpoint Implicit Flow: simple_user scope 1`] = `"openid some-custom-identity some-app-scope-1"`; + +exports[`Authorization Endpoint Implicit Flow: user_with_all_claim_types access token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "some-app", + "client_id": "implicit-flow-client-id", + "sub": "user_with_all_claim_types", + "idp": "local", + "some-app-user-custom-claim": "Oliver's Custom User Claim", + "some-app-scope-1-custom-user-claim": "Oliver's Scope Custom User Claim", + "scope": [ + "openid", + "some-custom-identity", + "some-app-scope-1" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`Authorization Endpoint Implicit Flow: user_with_all_claim_types id token 1`] = ` { "alg": "RS256", "typ": "JWT", @@ -142,7 +238,7 @@ exports[`Authorization Endpoint Implicit Flow 3`] = ` "aud": "implicit-flow-client-id", "nonce": "xyz", "s_hash": "ungWv48Bz-pBQUDeXa4iIw", - "sub": "user_with_standard_claims", + "sub": "user_with_all_claim_types", "idp": "local", "amr": [ "pwd" @@ -150,17 +246,23 @@ exports[`Authorization Endpoint Implicit Flow 3`] = ` } `; -exports[`Authorization Endpoint Implicit Flow: simple_user access token 1`] = ` +exports[`Authorization Endpoint Implicit Flow: user_with_all_claim_types scope 1`] = `"openid some-custom-identity some-app-scope-1"`; + +exports[`Authorization Endpoint Implicit Flow: user_with_custom_api_resource_claims access token 1`] = ` { "alg": "RS256", "typ": "JWT", "iss": "http://localhost:8080", + "aud": "some-app", "client_id": "implicit-flow-client-id", - "sub": "simple_user", + "sub": "user_with_custom_api_resource_claims", "idp": "local", + "some-app-user-custom-claim": "Sam's Custom User Claim", + "some-app-scope-1-custom-user-claim": "Sam's Scope Custom User Claim", "scope": [ "openid", - "some-custom-identity" + "some-custom-identity", + "some-app-scope-1" ], "amr": [ "pwd" @@ -168,19 +270,37 @@ exports[`Authorization Endpoint Implicit Flow: simple_user access token 1`] = ` } `; -exports[`Authorization Endpoint Implicit Flow: simple_user scope 1`] = `"openid some-custom-identity"`; +exports[`Authorization Endpoint Implicit Flow: user_with_custom_api_resource_claims id token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "implicit-flow-client-id", + "nonce": "xyz", + "s_hash": "ungWv48Bz-pBQUDeXa4iIw", + "sub": "user_with_custom_api_resource_claims", + "idp": "local", + "amr": [ + "pwd" + ] +} +`; + +exports[`Authorization Endpoint Implicit Flow: user_with_custom_api_resource_claims scope 1`] = `"openid some-custom-identity some-app-scope-1"`; exports[`Authorization Endpoint Implicit Flow: user_with_custom_identity_claims access token 1`] = ` { "alg": "RS256", "typ": "JWT", "iss": "http://localhost:8080", + "aud": "some-app", "client_id": "implicit-flow-client-id", "sub": "user_with_custom_identity_claims", "idp": "local", "scope": [ "openid", - "some-custom-identity" + "some-custom-identity", + "some-app-scope-1" ], "amr": [ "pwd" @@ -188,19 +308,37 @@ exports[`Authorization Endpoint Implicit Flow: user_with_custom_identity_claims } `; -exports[`Authorization Endpoint Implicit Flow: user_with_custom_identity_claims scope 1`] = `"openid some-custom-identity"`; +exports[`Authorization Endpoint Implicit Flow: user_with_custom_identity_claims id token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "implicit-flow-client-id", + "nonce": "xyz", + "s_hash": "ungWv48Bz-pBQUDeXa4iIw", + "sub": "user_with_custom_identity_claims", + "idp": "local", + "amr": [ + "pwd" + ] +} +`; + +exports[`Authorization Endpoint Implicit Flow: user_with_custom_identity_claims scope 1`] = `"openid some-custom-identity some-app-scope-1"`; exports[`Authorization Endpoint Implicit Flow: user_with_standard_claims access token 1`] = ` { "alg": "RS256", "typ": "JWT", "iss": "http://localhost:8080", + "aud": "some-app", "client_id": "implicit-flow-client-id", "sub": "user_with_standard_claims", "idp": "local", "scope": [ "openid", - "some-custom-identity" + "some-custom-identity", + "some-app-scope-1" ], "amr": [ "pwd" @@ -208,4 +346,20 @@ exports[`Authorization Endpoint Implicit Flow: user_with_standard_claims access } `; -exports[`Authorization Endpoint Implicit Flow: user_with_standard_claims scope 1`] = `"openid some-custom-identity"`; +exports[`Authorization Endpoint Implicit Flow: user_with_standard_claims id token 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "implicit-flow-client-id", + "nonce": "xyz", + "s_hash": "ungWv48Bz-pBQUDeXa4iIw", + "sub": "user_with_standard_claims", + "idp": "local", + "amr": [ + "pwd" + ] +} +`; + +exports[`Authorization Endpoint Implicit Flow: user_with_standard_claims scope 1`] = `"openid some-custom-identity some-app-scope-1"`; diff --git a/e2e/tests/__snapshots__/token-endpoint.spec.ts.snap b/e2e/tests/__snapshots__/token-endpoint.spec.ts.snap index 0df45d0..3e414ba 100644 --- a/e2e/tests/__snapshots__/token-endpoint.spec.ts.snap +++ b/e2e/tests/__snapshots__/token-endpoint.spec.ts.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Token Endpoint Client Credentials 1`] = ` +exports[`Token Endpoint Client Credentials: client-credentials-flow-client-id 1`] = ` { "alg": "RS256", "typ": "JWT", "iss": "http://localhost:8080", "aud": "some-app", - "client_id": "client-credentials-mock-client-id", + "client_id": "client-credentials-flow-client-id", "string_claim": "string_claim_value", "scope": [ "some-app-scope-1" @@ -17,3 +17,88 @@ exports[`Token Endpoint Client Credentials 1`] = ` ] } `; + +exports[`Token Endpoint Password: simple_user 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "password-flow-client-id", + "sub": "simple_user", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`Token Endpoint Password: user_with_all_claim_types 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "password-flow-client-id", + "sub": "user_with_all_claim_types", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`Token Endpoint Password: user_with_custom_api_resource_claims 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "password-flow-client-id", + "sub": "user_with_custom_api_resource_claims", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`Token Endpoint Password: user_with_custom_identity_claims 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "password-flow-client-id", + "sub": "user_with_custom_identity_claims", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`Token Endpoint Password: user_with_standard_claims 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "client_id": "password-flow-client-id", + "sub": "user_with_standard_claims", + "idp": "local", + "scope": [ + "openid" + ], + "amr": [ + "pwd" + ] +} +`; diff --git a/e2e/tests/__snapshots__/userinfo-endpoint.e2e-spec.ts.snap b/e2e/tests/__snapshots__/userinfo-endpoint.e2e-spec.ts.snap index 094646f..1dc037f 100644 --- a/e2e/tests/__snapshots__/userinfo-endpoint.e2e-spec.ts.snap +++ b/e2e/tests/__snapshots__/userinfo-endpoint.e2e-spec.ts.snap @@ -1,22 +1,156 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UserInfo Endpoint Invoke UserInfo endpoint 1`] = ` +exports[`UserInfo Endpoint Invoke UserInfo endpoint simple_user 1`] = ` Object { "sub": "simple_user", } `; -exports[`UserInfo Endpoint Invoke UserInfo endpoint 2`] = ` +exports[`UserInfo Endpoint Invoke UserInfo endpoint user_with_all_claim_types 1`] = ` Object { + "email": "oliver.hunter@gmail.com", + "name": "Oliver Hunter", + "some-custom-identity-user-claim": "Oliver's Custom User Claim", + "sub": "user_with_all_claim_types", +} +`; + +exports[`UserInfo Endpoint Invoke UserInfo endpoint user_with_custom_api_resource_claims 1`] = ` +Object { + "email": "sam.tailor@gmail.com", + "name": "Sam Tailor", + "sub": "user_with_custom_api_resource_claims", +} +`; + +exports[`UserInfo Endpoint Invoke UserInfo endpoint user_with_custom_identity_claims 1`] = ` +Object { + "email": "jack.sparrow@gmail.com", "name": "Jack Sparrow", "some-custom-identity-user-claim": "Jack's Custom User Claim", "sub": "user_with_custom_identity_claims", } `; -exports[`UserInfo Endpoint Invoke UserInfo endpoint 3`] = ` +exports[`UserInfo Endpoint Invoke UserInfo endpoint user_with_standard_claims 1`] = ` Object { + "email": "john.smith@gmail.com", + "email_verified": "true", "name": "John Smith", "sub": "user_with_standard_claims", } `; + +exports[`UserInfo Endpoint Retrieve user access token simple_user 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "some-app", + "client_id": "implicit-flow-client-id", + "sub": "simple_user", + "idp": "local", + "scope": [ + "openid", + "profile", + "email", + "some-custom-identity", + "some-app-scope-1" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`UserInfo Endpoint Retrieve user access token user_with_all_claim_types 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "some-app", + "client_id": "implicit-flow-client-id", + "sub": "user_with_all_claim_types", + "idp": "local", + "some-app-user-custom-claim": "Oliver's Custom User Claim", + "some-app-scope-1-custom-user-claim": "Oliver's Scope Custom User Claim", + "scope": [ + "openid", + "profile", + "email", + "some-custom-identity", + "some-app-scope-1" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`UserInfo Endpoint Retrieve user access token user_with_custom_api_resource_claims 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "some-app", + "client_id": "implicit-flow-client-id", + "sub": "user_with_custom_api_resource_claims", + "idp": "local", + "some-app-user-custom-claim": "Sam's Custom User Claim", + "some-app-scope-1-custom-user-claim": "Sam's Scope Custom User Claim", + "scope": [ + "openid", + "profile", + "email", + "some-custom-identity", + "some-app-scope-1" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`UserInfo Endpoint Retrieve user access token user_with_custom_identity_claims 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "some-app", + "client_id": "implicit-flow-client-id", + "sub": "user_with_custom_identity_claims", + "idp": "local", + "scope": [ + "openid", + "profile", + "email", + "some-custom-identity", + "some-app-scope-1" + ], + "amr": [ + "pwd" + ] +} +`; + +exports[`UserInfo Endpoint Retrieve user access token user_with_standard_claims 1`] = ` +{ + "alg": "RS256", + "typ": "JWT", + "iss": "http://localhost:8080", + "aud": "some-app", + "client_id": "implicit-flow-client-id", + "sub": "user_with_standard_claims", + "idp": "local", + "scope": [ + "openid", + "profile", + "email", + "some-custom-identity", + "some-app-scope-1" + ], + "amr": [ + "pwd" + ] +} +`; diff --git a/e2e/tests/authorization-endpoint.e2e-spec.ts b/e2e/tests/authorization-endpoint.e2e-spec.ts index e85fe71..961870e 100644 --- a/e2e/tests/authorization-endpoint.e2e-spec.ts +++ b/e2e/tests/authorization-endpoint.e2e-spec.ts @@ -54,7 +54,7 @@ describe('Authorization Endpoint', () => { test.each(testCases)('Implicit Flow', async (user: User) => { const parameters = { client_id: implicitFlowClient.ClientId, - scope: 'openid some-custom-identity', + scope: 'openid some-custom-identity some-app-scope-1', response_type: 'id_token token', redirect_uri: implicitFlowClient.RedirectUris?.[0].replace('*', 'www'), state: 'abc', @@ -77,7 +77,7 @@ describe('Authorization Endpoint', () => { const idToken = query['id_token']; expect(typeof idToken).toEqual('string'); const decodedIdToken = decodeJWT(idToken as string); - expect(decodedIdToken).toMatchSnapshot(); + expect(decodedIdToken).toMatchSnapshot(`${user.Username} id token`); const accessToken = query['access_token']; expect(typeof accessToken).toEqual('string'); diff --git a/e2e/tests/token-endpoint.spec.ts b/e2e/tests/token-endpoint.spec.ts index de64b42..270c9c5 100644 --- a/e2e/tests/token-endpoint.spec.ts +++ b/e2e/tests/token-endpoint.spec.ts @@ -3,24 +3,49 @@ import * as dotenv from 'dotenv'; import axios from 'axios'; import { decode } from 'jws'; +import users from '../config/user-configuration.json'; import clients from '../config/clients-configuration.json'; -import type { Client } from '../types'; +import type { Client, User } from '../types'; + +const testCases: User[] = users.sort((u1, u2) => (u1.Username < u2.Username ? -1 : 1)); describe('Token Endpoint', () => { - let client: Client; + let clientCredentialsFlowClient: Client; + let passwordFlowClient: Client; beforeAll(() => { dotenv.config(); - client = clients.find(c => c.ClientId === 'client-credentials-mock-client-id'); - expect(client).toBeDefined(); + clientCredentialsFlowClient = clients.find(c => c.ClientId === 'client-credentials-flow-client-id'); + expect(clientCredentialsFlowClient).toBeDefined(); + + passwordFlowClient = clients.find(c => c.ClientId === 'password-flow-client-id'); + expect(passwordFlowClient).toBeDefined(); }); test('Client Credentials', async () => { const parameters = { - client_id: client.ClientId, - client_secret: client.ClientSecrets?.[0], + client_id: clientCredentialsFlowClient.ClientId, + client_secret: clientCredentialsFlowClient.ClientSecrets?.[0], grant_type: 'client_credentials', - scope: client.AllowedScopes[0], + scope: clientCredentialsFlowClient.AllowedScopes, + }; + + const response = await axios.post(process.env.OIDC_TOKEN_URL, querystring.stringify(parameters)); + + expect(response).toBeDefined(); + expect(response.data.access_token).toBeDefined(); + const token = decode(response.data.access_token); + + expect(token).toMatchSnapshot(clientCredentialsFlowClient.ClientId); + }); + + test.each(testCases)('Password', async (user: User) => { + const parameters = { + client_id: passwordFlowClient.ClientId, + username: user.Username, + password: user.Password, + grant_type: 'password', + scope: passwordFlowClient.AllowedScopes, }; const response = await axios.post(process.env.OIDC_TOKEN_URL, querystring.stringify(parameters)); @@ -29,6 +54,6 @@ describe('Token Endpoint', () => { expect(response.data.access_token).toBeDefined(); const token = decode(response.data.access_token); - expect(token).toMatchSnapshot(); + expect(token).toMatchSnapshot(user.Username); }); }); diff --git a/e2e/tests/userinfo-endpoint.e2e-spec.ts b/e2e/tests/userinfo-endpoint.e2e-spec.ts index 7acca9f..276ec3f 100644 --- a/e2e/tests/userinfo-endpoint.e2e-spec.ts +++ b/e2e/tests/userinfo-endpoint.e2e-spec.ts @@ -2,6 +2,7 @@ import * as querystring from 'querystring'; import * as dotenv from 'dotenv'; import { chromium, Page, Browser } from 'playwright-chromium'; import axios from 'axios'; +import { decode as decodeJWT } from 'jws'; import users from '../config/user-configuration.json'; import clients from '../config/clients-configuration.json'; @@ -39,7 +40,7 @@ describe('UserInfo Endpoint', () => { test(`Retrieve user access token ${user.SubjectId}`, async () => { const parameters = { client_id: implicitFlowClient.ClientId, - scope: 'openid profile some-custom-identity', + scope: 'openid profile email some-custom-identity some-app-scope-1', response_type: 'id_token token', redirect_uri: implicitFlowClient.RedirectUris?.[0].replace('*', 'www'), state: 'abc', @@ -62,9 +63,11 @@ describe('UserInfo Endpoint', () => { const token = query['access_token']; expect(typeof token).toEqual('string'); accessToken = token as string; + const decodedAccessToken = decodeJWT(accessToken); + expect(decodedAccessToken).toMatchSnapshot(); }); - test('Invoke UserInfo endpoint', async () => { + test(`Invoke UserInfo endpoint ${user.SubjectId}`, async () => { const response = await axios.get(process.env.OIDC_USERINFO_URL, { headers: { authorization: `Bearer ${accessToken}` }, }); diff --git a/src/Config.cs b/src/Config.cs index 6461bf3..c317a13 100644 --- a/src/Config.cs +++ b/src/Config.cs @@ -72,7 +72,7 @@ public static IEnumerable GetApiResources() } apiResourcesStr = File.ReadAllText(apiResourcesFilePath); } - var apiResources = JsonConvert.DeserializeObject>(apiResourcesStr, new SecretConverter(), new ClaimJsonConverter()); + var apiResources = JsonConvert.DeserializeObject>(apiResourcesStr, new SecretConverter()); return apiResources; } diff --git a/src/ConfigurableProfileService.cs b/src/ConfigurableProfileService.cs new file mode 100644 index 0000000..673a156 --- /dev/null +++ b/src/ConfigurableProfileService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using IdentityServer4.Services; +using IdentityServer4.Test; + +namespace OpenIdConnectServer +{ + internal class ConfigurableProfileService : IProfileService + { + private readonly IEnumerable _users; + + public ConfigurableProfileService() + { + _users = Config.GetUsers(); + } + + public Task GetProfileDataAsync(ProfileDataRequestContext context) + { + var userName = context.Subject.GetSubjectId(); + var user = this._users.FirstOrDefault(u => u.Username == userName); + if (user != null) + { + var claims = context.FilterClaims(user.Claims); + context.AddRequestedClaims(claims); + } + return Task.CompletedTask; + } + + public Task IsActiveAsync(IsActiveContext context) + { + var userName = context.Subject.GetSubjectId(); + var user = this._users.FirstOrDefault(u => u.Username == userName); + context.IsActive = user?.IsActive ?? false; + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index 000c369..36cef98 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -24,7 +24,8 @@ public void ConfigureServices(IServiceCollection services) .AddInMemoryApiScopes(Config.GetApiScopes()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers()) - .AddRedirectUriValidator(); + .AddRedirectUriValidator() + .AddProfileService(); var aspNetServicesOptions = Config.GetAspNetServicesOptions(); AspNetServicesHelper.ApplyAspNetServicesOptions(services, aspNetServicesOptions);