Keycloak single sign-on via Microsoft AD

Keycloak single sign-on via Microsoft AD

Keycloak Angular

This library helps you to use keycloak-js in Angular applications. A Keycloak Service that wraps the keycloak-js methods to be used in Angular, giving extra functionalities to the original functions and adding new methods to make it easier to be consumed by Angular applications.

Initialization

In order to make sure Keycloak is initialized when your application is bootstrapped you will have to add an APP_INITIALIZER provider to your AppModule. This provider will call the initializeKeycloak factory function shown below which will set up the Keycloak service so that it can be used in your application.

The option "check-sso" will authenticate the client if the user is already logged-in, if the user is not logged in the browser will be redirected back to the application and remain unauthenticated.

keycloak.init({
config: {
url: 'http://localhost:8080/auth',
realm: 'your-realm',
clientId: 'your-client-id',
},
initOptions: {
onLoad: 'check-sso',
},
});

Silent Check SSO

With this feature enabled, your browser won’t do a full redirect to the Keycloak server and back to your application, but this action will be performed in a hidden iframe, so your application resources only need to be loaded and parsed once by the browser when the app is initialized and not again after the redirect back from Keycloak to your app. This is particularly useful in the case of SPAs (Single Page Applications).

This option might not be enabled due to X-frame-options set for denial accessing at your application domain inside an iframe. This requires a configuration change at Nginx to enable this feature.

Error:

The loading of “https://xxxxx.dev.net/assets/html/silent-check-sso.html#state=03f3841d-67c2-4949-9641-b05f8b2b02e9&session_state=e52f107e-4bce-4a1d-86c7-d88b11159a14&code=cc6778b6-9a1d-4a63-875c-8e1af737a5f6.e52f107e-4bce-4a1d-86c7-d88b11159a14.xxx-ui” in a frame is denied by “X-Frame-Options“ directive set to “DENY“.

Login

The login function redirects to the home page if successfully authenticated if the user is not authenticated will be redirected to the prior identity provider for authentication.

idphint is used to tell Keycloak to skip showing the login page and automatically redirect to the specified identity provider instead.

Specifies the URI to redirect post login

async login(options: Keycloak.KeycloakLoginOptions = {})

Options:

interface KeycloakLoginOptions
{
scope?: string;
redirectUri?: string;
prompt?: 'none'|'login';
action?: string;
maxAge?: number;
loginHint?: string;
idpHint?: string;
locale?: string;
cordovaOptions?: { [optionName: string]: string };
}
public login(): Promise<void> {
    return this.keycloak.login({
      scope: 'openid offline_access'
    });
  }
this.keyCloak
.initialiseKeycloak()
.then(success => {
        if (!success) {
          this.keyCloak.login().catch(() => {
            console.error('Login failed. Try Again !');
          });
        } else {
          this.gotoLandingScreen();
        }
})
.catch(failure => {
console.error("couldn't get Login URL. Try Again !");
});

Logout

Specifies the URI to redirect to after logout

async logout(redirectUri?: string)

Token Refresh The token refresh mechanism remains the same. The existing token refresh implementation is not modified for SSO.

Exception

The keycloak redirects to an appropriate URL on successful authentication. In case of exception, expect the same to redirect to login URL to enable local user login. The keycloak initialization block is wrapped with a catch block and it's quite capable to handle exceptions upon its library functions. When keycloak goes down, the request URL from the browser returns to "not reachable" exception. This could be solved by having a standalone API call to determine the liveness of keycloak runner. The liveness check is achieved by requesting 'GET' operation on "https://{hostname}/auth/realms/{reakm}/.well-known/openid-configuration" OpenID configuration URL without authorization.

Configuration

realm: 'xxxx-C', clientId: 'xxx-ui' Keycloak Url: xxx-keycloak.dev.srv.da.nsn-rdnet.net/auth

Offline Token

Authorization Code Flow The OAuth 2.0 authorization code grant is used in keycloak configs that is installed on a device to gain access to protected resources. The flow enables apps to securely acquire access_tokens that can be used to access resources secured by the Microsoft identity platform, as well as refresh tokens to get additional access_tokens, and ID tokens for the signed in user.

Request Auth Code

The authorization code flow begins with the client directing the user to the /authorize endpoint. In this request, the client requests the openid, offline_access, and other permissions from the user. The request to 'auth code' use URL forwarding technique to give another URL address back to the page.

https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
public requestAuthCodeURL(): string {
    const realm = environment.keycloakconfig.realm;
    const client_id = environment.keycloakconfig.clientId;
    const scope = 'openid%20offline_access';
    const redirect_uri = `${window.location.origin}/assets/html/oauth-token.html`;
    const path = `realms/${realm}/protocol/openid-connect/auth?response_type=code&client_id=${client_id}&scope=${scope}&redirect_uri=${redirect_uri}`;
    const keyCloakUrl = this.singleSignOnService.getKeycloakURL();
    return `${keyCloakUrl}${path}`;
  }

Successful Response

GET http://localhost? code=AwABAAAAvPM1KaPlrEqdFSBzjqfTGBCmLdgfSTLEMPGYuNHSUYBrq... &state=12345

Request an Offline Token

Now that you've acquired an authorization_code and have been granted permission by the user, you can redeem the code for an access_token to the desired resource. Do this by sending a POST request to the /token endpoint

HTTP Post url: http://{hostname}/auth/realms/{realm}/protocol/openid-connect/token

Form Data:

grant_type:authorization_code
client_id:xx-m
code:3f982d41-806b-49a2-99e7-68670867c3ce.24cca130-c51e-4dc8-8d7e-801f2e79d316.28d1baff-b5df-4ad1-8b18-c4552f35c79a

Response

{
"access_token": "eyJhbGciOiJIUzI1NiIsIn",
"expires_in": 300,
"refresh_expires_in": 28723,
"refresh_token": "eyJhbGciOiJIUzI1Ni",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "24cca130-c51e-4dc8-8d7e-801f2e79d316",
"scope": "profile email offline_access"
}

Request Offline Token Via browser

<script type="text/javascript" language="javascript">
        'use strict';

        window.addEventListener('DOMContentLoaded', getAuthCode);

        function getAuthCode() {
            try {
                const params = new URLSearchParams(window.location.search);
                const authCode = params.get('code');
                fetchTokenByCode(authCode);
            }
            catch (error) {
                console.log('error', error);
            }
        }

        function fetchTokenByCode(authCode) {
            try {
                const tokenEndPoint = "/auth/realms/xxx-C/protocol/openid-connect/token";
                const grantType = "authorization_code";
                const clientId = "xxxx-ui";
                const fileName = "jwt_token.txt";
                const redirectURI = `${window.location.origin}/assets/html/oauth-token.html`;
                if (!authCode)
                    return;
                let headerOptions = new Headers();
                headerOptions.append("Content-Type", "application/x-www-form-urlencoded");
                let urlencoded = new URLSearchParams();
                urlencoded.append("grant_type", grantType);
                urlencoded.append("client_id", clientId);
                urlencoded.append("code", authCode);
                urlencoded.append("redirect_uri", redirectURI);
                let requestOptions = {
                    method: 'POST',
                    headers: headerOptions,
                    body: urlencoded
                };
                const keyCloakURL = getKeyCloakURL(tokenEndPoint);
                fetch(keyCloakURL, requestOptions)
                    .then(response => {
                        console.log(response);
                        return response.json();
                    })
                    .then(result => {
                        console.log(result);
                        downloadPlainText(fileName, result["refresh_token"]);
                    })
                    .catch(error => {
                        console.log('error', error);
                        window.close();
                    });
            }
            catch (error) {
                throw "Error occurred at post call.";
            }
        }

        function getKeyCloakURL(tokenEndPoint) {
            try {
                const host = window.location.hostname;
                const strArray = host.split('.');
                if (strArray && strArray.length > 1) {
                    const modifiedHost = strArray.reduce((prev, curr, index) => {
                        let temp = '';
                        if (index == 0) {
                            temp = 'auth';
                        } else {
                            temp = prev + '.' + curr;
                        }
                        return temp;
                    }, '');                    
                    return `https://${modifiedHost}${tokenEndPoint}`;
                }
                return `${window.location.origin}${tokenEndPoint}`;
            } catch (error) {
                console.log('error', error);
                return `${window.location.origin}${tokenEndPoint}`;
            }
        }

        function downloadPlainText(fileName, data) {
            try {
                let element = document.createElement('a');
                element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data));
                element.setAttribute('download', fileName);
                element.style.display = 'none';
                document.body.appendChild(element);
                element.click();
                document.body.removeChild(element);
                setTimeout(() => {
                   window.close();
                }, 0);
            }
            catch (error) {
                console.log('error', error);
                throw "Error occurred while downloading the file.";
            }
        }
    </script>

Active Token Count

The count determines the number of tokens downloaded per login session.

URL - api/v3/users/offline-sessions

Request - api/v3/users/offline-sessions?client_id={clientid}&realm_name={realm}'

Response

{
"status": {
"status_code": "UNSPECIFIED",
"status_description": {
"description_code": "NOT_SPECIFIED",
"description": "string"
}
},
"offline_token_count": 0
}

Revoke All Token

URL - api/v3/users/offline-sessions

Request

{
"client_id": "string",
"realm_name": "string"
}

Response

{
"status": {
"status_code": "SUCCESS",
"status_description": {
"description_code": "NOT_SPECIFIED",
"description": "string"
}
}
}