In the previous instalment I demonstrated Keycloak in action as an SAML WebSSO Identity Provider. However, and likewise SAP IAS, Azure AD and many other IDPs, each Keycloak tenant (realm) can act as an OpenID Connect (OIDC) provider so you can create OIDC clients (applications) for user authentication. The OIDC protocol supports a number of grant types that can be implemented to authenticate a user; with the preferred type being the authorization code flow that is supported by Keycloak. |
Disclaimer:
|
That's a relatively straightforward operation that you will do in your realm (aka tenant), as depicted below: Good to know:
Next we may want to (re-)generate the client secret. (Credentials tab) And finally we can conveniently download the OIDC client settings in json format. (Installation tab), as shown below: |
{
"realm": "ateam-isveng",
"auth-server-url": "https://keycloak<cluster>/auth/",
"ssl-required": "external",
"resource": "quovadis-oidc",
"credentials": {
"secret": "<client_secret>"
},
"confidential-port": 0
}
Please note the resource, "quovadis-oidc" is the client_id.
We can discover keycloak OIDC provider metadata by appending the /.well-known/openid-configuration suffix to the provider tenant (realm) path as follows:
Here goes the custom realm OIDC metadata discovery endpoint:https://keycloak.<cluster>/auth/realms/ateam-isveng/.well-known/openid-configuration |
// both redacted and truncated...
//
https://keycloak.<cluster>/auth/realms/ateam-isveng/.well-known/openid-configuration
{
"issuer": "https://keycloak/auth/realms/ateam-isveng",
"authorization_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/auth",
"token_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/token",
"introspection_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/userinfo",
"end_session_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/logout",
"jwks_uri": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/certs",
"check_session_iframe": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:openid:params:grant-type:ciba"
],
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
}
"authorization_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/auth", "token_endpoint": "https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect/token", |
//--------------------------------------------------------------------------------------------------------
// https://www.npmjs.com/package/openid-client
// https://github.com/jcawley5/oidc-sample-app/blob/master/app/auth/sapOIDC.js
const { Issuer } = require('openid-client');
const { generators } = require('openid-client');
const scopes_supported = "openid email";
var client_keycloak;
//-------------------------------------------------------------------------------------------------------------------------------------
// store the code_verifier in your framework's session mechanism, if it is a cookie based solution
// it should be httpOnly (not readable by javascript) and encrypted.
//
const code_verifier = generators.codeVerifier();
//-------------------------------------------------------------------------------------------------------------------------------------
//
const credentials_keycloak = {
client: {
id: 'quovadis-oidc',
secret: '<secret>'
},
auth: {
authorizeHost: 'https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect',
authorizePath: 'auth',
tokenHost: 'https://keycloak.<cluster>/auth/realms/ateam-isveng/protocol/openid-connect',
tokenPath: 'token',
},
options: {
authorizationMethod: 'body'
}
};
//-------------------------------------------------------------------------------------------------------------------------------------
//
const keycloak_issuerUrl = 'https://keycloak.<cluster>/auth/realms/ateam-isveng';
//-------------------------------------------------------------------------------------------------------------------------------------
const redirecturi_keycloak = 'https://poster.<client-cluster>/auth/keycloak';
//-------------------------------------------------------------------------------------------------------------------------------------
//
async function discover_openid_issuer(issuerUrl) {
try {
issuer = await Issuer.discover(issuerUrl);
console.log("Discovered issuer %s %O", issuer.issuer, issuer.metadata);
return issuer;
} catch (error) {
console.log(error);
return null;
}
}
//-------------------------------------------------------------------------------------------------------------------------------------
//
function init_issuer_clients(keycloakIssuer) {
//https://tools.ietf.org/html/rfc6749#section-2.3.1
if (keycloakIssuer !== null) {
client_keycloak = new keycloakIssuer.Client({
client_id: credentials_keycloak.client.id,
client_secret: credentials_keycloak.client.secret,
redirect_uris: [redirecturi_keycloak],
response_types: ["code"],
token_endpoint_auth_method: "client_secret_post",
});
}
console.log(client_keycloak);
return client_keycloak);
}
//-------------------------------------------------------------------------------------------------------------------------------------
//
function getmetadata(event) { // ?issuer=keycloak
if (typeof (event.extensions.request.query.issuer) !== 'undefined') {
if (event.extensions.request.query.issuer === 'keycloak') {
return JSON.stringify(keycloakIssuer.metadata, null, 2);
}
}
return JSON.stringify(sapIssuer.metadata, null, 2);
}
//-------------------------------------------------------------------------------------------------------------------------------------
//
async function authorizationcode_openid_client(event) {
let client = client_keycloak; // current client
let scope = scopes_supported;
try {
const code_challenge = generators.codeChallenge(code_verifier);
let authorizationUri = client.authorizationUrl({
scope: scope,
access_type: 'offline',
prompt: 'consent',
//resource: redirect_url,
code_challenge,
code_challenge_method: 'S256',
});
console.log("authorizationcode_openid_client: authorizationUri: " + authorizationUri);
event.extensions.response.redirect(authorizationUri);
return authorizationUri;
} catch (error) {
console.log(error);
}
}
//-------------------------------------------------------------------------------------------------------------------------------------
//
async function callback_openid_client(event, redirecturi_id = redirecturi_keycloak) {
// https://github.com/panva/node-openid-client/blob/main/docs/README.md#clientcallbackredirecturi-param...
let client = client_keycloak; // deafult client
// Returns recognized callback parameters from a provided input.
const params = client.callbackParams(event.extensions.request);
console.log ('params: ', params);
try {
tokenSet = await client.callback(redirecturi_id, params, { code_verifier }, { exchangeBody: {access_type: 'offline',}});
console.log('received and validated tokens %j', tokenSet);
console.log('validated ID Token claims %j', tokenSet.claims());
console.log("callback_openid_client: " + JSON.stringify(tokenSet));
// you might securely save the token in a secret place...like a config map for instance or in a database or in a vault...
//await k8spatchcm();
// for the sake of simplicity the token is returned both as a param to another endpoint and a nd as a safe server-side cookie
let token_cookie_header = 'token_keycloak';
event.extensions.response.clearCookie(token_cookie_header);
event.extensions.response.cookie(token_cookie_header, tokenSet.id_token, { httpOnly: true, secure: true, sameSite: 'none' }); // tokenSet.access_token
event.extensions.response.set(token_cookie_header, tokenSet.id_token); // tokenSet.access_token
return event.extensions.response.redirect('/logonresponse_openid?token=' + tokenSet.id_token); // tokenSet.access_token
}
catch (err) {
console.log("An error occured: ", err);
return err;
};
}
//-------------------------------------------------------------------------------------------------------------------------
//
function logonresponse_openid(event) {
console.log("inside logonresponse_openid...");
let token = "";
// is token passed via a parameter from the callback ?
if (typeof (event.extensions.request.query.token) !== "undefined") {
token = event.extensions.request.query.token;
}
else {
console.log(event.extensions.request.get('cookie'));
var cookies = cookie.parse(event.extensions.request.get('cookie') || '');
token = cookies.token_openid;
//let accessToken = cookies.serialized;
}
console.log("logonresponse_openid: token: ", token);
return token;
}
//--------------------------------------------------------------------------
//
var keycloakIssuer, initialized = false;
//--------------------------------------------------------------------------
//
module.exports = {
main: async function (event, context) {
if (initialized === false) {
initialized = true;
keycloakIssuer = await discover_openid_issuer(keycloak_issuerUrl);
client = init_issuer_clients(keycloakIssuer);
}
switch (event.extensions.request.path) {
case '/openid': {
return authorizationcode_openid_client(event);
}
case '/auth/keycloak': {
return callback_openid_client(event, redirecturi_keycloak);
}
case '/logonresponse_openid' : {
return logonresponse_openid(event);
}
case '/getmetadata' : {
return getmetadata(event);
}
}
}
}
}
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2aldHUHFtMTBpcnhadjVELXYwMHdmVVlXejJpbkFiU0lTR
..............................(truncated).......................................
c7VbWJeIdKqh6Vyry8NQJovD6D1mQMjJjcdDZhCQdILAXj3Kr6Fp3MY647FKJd3DcPO_ucKytmwuoEtbWPiERe3rk3jNWnGynPUpupOgit8zRPZpw
// redacted
HEADER:ALGORITHM & TOKEN TYPE
{
"alg": "RS256",
"typ": "JWT",
"kid": "<kid>"
}
PAYLOAD:DATA
{
"exp": 1629753985,
"iat": 1629717986,
"auth_time": 1629717985,
"jti": "<jti>",
"iss": "https://<cluster>/auth/realms/ateam-isveng",
"aud": "quovadis-oidc",
"sub": "<sub>",
"typ": "ID",
"azp": "quovadis-oidc",
"session_state": "<session_state>",
"at_hash": "<at_hash>",
"acr": "1",
"sid": "<sid>",
"email_verified": true,
"name": "<name>",
"preferred_username": "<preferred_username>",
"given_name": "<given_name>",
"family_name": "<family_name>",
"email": "<email>"
}
The above code is provided "as is". It can be used as a coding template with virtually any other OIDC provider. However, as all OIDC providers have their own quirks small coding adjustment may be required... For the sake of brevity I omitted the refresh token logic from the above code.... If you would like to off-load this coding effort you might want to consider existing public libraries for instance: https://github.com/oauth2-proxy/oauth2-proxy |
The following blog has a very good a good intro to Open ID Connect (OIDC) protocol. Quoting after jamie.cawley:Open ID Connect (OIDC) provides a simple layer on top of oAuth 2.0 to support user authentication, providing login and profile information in the form of an encoded JSON Web Token(JWT). |
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
13 | |
10 | |
10 | |
7 | |
6 | |
5 | |
5 | |
5 | |
5 | |
4 |