top of page

Understanding Keycloak: A Guide to Access Token, Refresh Token, and ID Token

  • pranaypourkar
  • Jun 2, 2023
  • 8 min read

Updated: Jun 26, 2023


Authentication and Authorization are two concepts in Keycloak that governs access to resources based on the identity of users.


Authentication: Authentication in Keycloak involves verifying the identity of a user. It ensures that users are who they claim to be before granting them access to protected resources. Keycloak supports various authentication mechanisms, including username/password, social login (e.g., Google, Facebook), and multi-factor authentication.

For Example: We have a web application protected by Keycloak. When a user tries to access a secured page, the application redirects them to Keycloak for authentication. The user enters their username and password on the Keycloak login page. Keycloak verifies the credentials and, upon successful authentication, issues an access token that represents the user's identity.


Authorization: Authorization in Keycloak involves determining what resources a user can access and what actions they can perform. It defines the permissions and privileges associated with different roles and assigns those roles to users. Keycloak provides role-based access control (RBAC) to manage authorization.

For Example: After a user has been authenticated, Keycloak can enforce authorization rules based on their assigned roles. For instance, let's consider an e-commerce application where users can be assigned roles such as "customer" or "admin". A customer with the "customer" role may have permission to view products, add items to their cart, and place orders. On the other hand, an admin with the "admin" role may have additional privileges to manage products, view customer details, and perform administrative tasks. Keycloak ensures that only users with the appropriate roles can access specific resources or perform specific actions.



Keycloak uses token-based authentication, where tokens (such as Access Token, Refresh Token, and ID Token) are used to verify the identity of the client and the user. Access Token, Refresh Token, and ID Token are all used in the context of authentication and authorization, particularly in the OAuth 2.0 and OpenID Connect protocols in Keycloak.


Access Token: An access token is a credential that is issued by an authentication server such as Keycloak to a client application after successful authentication. It represents the identity of the authenticated user and contains information about the user and their granted permissions and scope. The access token is typically short-lived and has an expiration time. It is used by the client to access protected resources or make API requests on behalf of the user.


Refresh Token: A refresh token is a long-lived credential that is also issued by the authentication server during the authentication process. It is used to obtain a new access token after the current access token expires. When the access token expires, the client application can use the refresh token to request a new access token without requiring the user to re-authenticate. Refresh tokens are typically longer-lived than access tokens and are securely stored on the client side. They should be kept confidential and transmitted securely.


ID Token: An ID token is specific to the OpenID Connect protocol, an authentication layer built on top of OAuth 2.0. It is a JSON Web Token (JWT) that contains identity information about the authenticated user, such as their name, email address, and other profile information. The ID token is issued by the identity provider such as Keycloak during the authentication process and is consumed by the client application. It provides information about the authenticated user and can be used for authentication and user information verification. If we set the "openid" scope when requesting an access token from the Keycloak authorization server, we will receive an ID token along with the access token.

If we need to map different attributes, such as an account ID, between two different applications for single sign-on (SSO) purposes, we can utilize the ID token in Keycloak. We can configure attribute mappings to include additional attributes in the ID token



All the above tokens are typically encoded using a specific algorithm to ensure their integrity and security during transmission and storage. In the context of Keycloak, the tokens are encoded as JSON Web Tokens (JWTs).


Keycloak supports various algorithms for token encoding and signing, including:

  • Symmetric Algorithms: Keycloak supports symmetric algorithms like HMAC-SHA256 and HMAC-SHA512 for signing the tokens. Symmetric algorithms use a single shared secret key to both encrypt and decrypt data.

  • Asymmetric Algorithms: Keycloak also supports asymmetric algorithms like RSA and ECDSA for signing the tokens. These algorithms use a public-private key pair, where the Keycloak server holds the private key and the client application holds the public key. The public key is used for encryption, while the private key is used for decryption.

Some of the Symmetric and Asymmetric Algorithms are listed below. These algorithms represent a mix of symmetric (HMAC) and asymmetric (RSA, ECDSA) cryptographic operations. The choice of algorithm depends on factors such as security requirements, key management, and the capabilities of the client applications or systems that will validate the tokens.


Symmetric Algorithms:

  1. HS256: HMAC-SHA256 symmetric signing algorithm

  2. HS384: HMAC-SHA384 symmetric signing algorithm

  3. HS512: HMAC-SHA512 symmetric signing algorithm

Asymmetric Algorithms:

  1. RS256: RSA-SHA256 asymmetric signing algorithm

  2. RS384: RSA-SHA384 asymmetric signing algorithm

  3. RS512: RSA-SHA512 asymmetric signing algorithm

  4. ES256: ECDSA-SHA256 asymmetric signing algorithm

  5. ES384: ECDSA-SHA384 asymmetric signing algorithm

  6. ES512: ECDSA-SHA512 asymmetric signing algorithm


The specific algorithm used for encoding and signing the tokens depends on the configuration of the Keycloak server and the chosen realm settings. We can configure the algorithm preferences in the Keycloak admin console under the realm settings and token settings.

ree

Default Signature Algorithm parameter means default algorithm used to sign tokens for the realm. For example RS256 is selected in below screenshot.


RS256 is a widely used algorithm for signing JSON Web Tokens (JWTs) in the context of the JSON Web Signature (JWS) specification. It is part of the RSA family of algorithms, where the number "256" represents the size of the RSA key used, which is 256 bits.

In the JWT context, RS256 is responsible for signing the token, which ensures its integrity and authenticity. The process involves encoding the token's header and payload sections into a compact string format called the "Signing Input," and then applying an RSA digital signature using the private key corresponding to the RSA public key associated with the token issuer. This signature is appended to the token, forming the JWS. On the receiving side, the JWT can be validated by using the corresponding RSA public key to verify the signature. This ensures that the token has not been tampered with and originates from a trusted source


Encoding of the token's content itself is not specific to the RS256 algorithm. Encoding is performed to ensure that the token can be safely transmitted across different systems or mediums, but it is not directly related to the signing process performed by RS256. By default, Keycloak encodes the JWTs using Base64 encoding for the token's header and payload sections.

ree

Public Key or Certificate is available in "Keys" Section.

  1. Public Key: The public key is made available for download so that it can be used by client applications or other services to verify the authenticity and integrity of JWTs issued by Keycloak. When a JWT is signed using a private key, the corresponding public key is required to validate the signature. By providing the public key, Keycloak enables clients to verify the JWT signatures independently.

  2. Certificate: In some scenarios, it may be more convenient or necessary to use a certificate instead of directly using the public key. The certificate includes the public key and additional information, such as the issuer, validity period, and certificate authority (CA) that issued the certificate. Providing the certificate allows clients to easily retrieve the public key and other relevant information in a standardized format.

ree



JWT Tokens can be decoded easily on this page - https://jwt.io/


JWTs consist of three parts:

  • Header

  • Payload

  • Signature

The header specifies the algorithm used to sign the token, while the payload contains the token claims, such as the user's identity and granted permissions. The signature is used to verify the integrity of the token.


A decoded Access Token looks like below.

​Header

Payload

Signature


ree


ree


ree

A decoded Refresh Token looks like below.

Header

Payload

Signature


ree


ree


ree


A decoded ID Token looks like below.

Header

Payload

Signature


ree


ree


ree

Let's understand some of the fields included in the token

​​​​

Header Section:

Fields

Description

alg

The "alg" (algorithm) field specifies the cryptographic algorithm used to sign the JWT. In this case, "RS256" indicates that the token is signed using the RSA algorithm with SHA-256 hashing.

typ

The "typ" (type) field indicates the type of token, which is "JWT" in our case.

kid

The "kid" (key ID) field represents the identifier of the key used to sign the JWT. This can be used to identify the key used for verification on the receiving end.

Payload Section:

Fields

Description

exp

The "typ" (type) field indicates the type of token, in this case, it's a "Bearer" token.

iat

The "iat" (issued at) field represents the timestamp (in seconds) when the token was issued.

auth_time

Represents the time when the user was authenticated by the identity provider. It indicates the timestamp at which the authentication event occurred.

jti

The "jti" (JWT ID) field is a unique identifier for the token

iss

The "iss" (issuer) field specifies the issuer URL or endpoint that issued the token.

aud

Represents the audience for which the token is intended. It specifies the intended recipient or recipients of the JWT.

sub

The "sub" (subject) field contains the identifier of the subject (user) for whom the token was issued.

typ

The "typ" (type) field indicates the type of token, in this case, it's a "Bearer" token.

azp

The "azp" (authorized party) field specifies the client or application that is authorized to use the token.

session_state

The "session_state" field represents the unique identifier for the user's session.

at_hash

This claim can be present in ID Token and is used to verify the integrity of an access token when it is used in certain contexts, such as OpenID Connect (OIDC) authentication flows.

acr

The "acr" (authentication context class reference) field indicates the level of authentication used during token issuance.

sid

The "sid" (session ID) field is the session identifier associated with the token.

email_verified

The "email_verified" field indicates whether the user's email has been verified (true or false).

name

The "name" field contains the user's full name.

preferred_username

The "preferred_username" field specifies the user's preferred username.

given_name

The "given_name" field contains the user's given name or first name.

family_name

The "family_name" field represents the user's family name or last name.

authorities

The "authorities" field lists the roles or authorities assigned to the user.

email

The "internal_scope" field contains internal scopes or permissions associated with the user.



Let's understand how we can verify whether a token (say ID Token) is valid and not tampered.


There are different ways to parse and validate JWT Tokens

  1. Manual Parsing and Validation: In this approach, we have to manually parse the JWT token by splitting it into its three components (header, payload, and signature) using a base64 decoding mechanism. Once split, we have to inspect the token's claims and validate the signature using the token's signing algorithm and the corresponding key. We have to write the logic by ourselves with the help of RFC 7519: JSON Web Token (JWT)

  2. JWT Libraries: Utilize JWT libraries available in your programming language or framework. These libraries provide built-in methods to parse and validate JWT tokens, making the process easier and more robust. Libraries for different framework/language is available at JWT.IO - JSON Web Tokens Libraries

  3. Identity Provider SDKs: Many identity providers offer SDKs that handle JWT parsing and validation as part of their authentication libraries. For example, libraries like Auth0 SDKs, Okta SDKs, or Azure AD libraries often include methods to validate JWT tokens issued by their respective identity providers.

  4. Framework Integration: Some web frameworks have built-in support for JWT token handling and validation. These frameworks provide middleware or modules that handle the parsing, validation, and authentication of JWT tokens automatically.

  5. Online Validation Tools: Use online JWT validation tools or libraries to perform validation checks without writing code. For example using this site - JWT.IO


We will be using Java JWT Library - Nimbus-JOSE-JWT Bitbucket and a

sample Spring Boot project to verify ID Token Signature of a Valid and Forged Token.

Fetch the certificate details using Certs endpoint (/realms/employee/protocol/openid-connect/certs) and use it to verify the signature of the JWT Tokens

Let's start the keycloak and mysql service using docker-compose.

version: "3.9"
# https://docs.docker.com/compose/compose-file/

services:

  # If mysql volume is already created and need to change the initial setup, 
  # remove the volume and restart the container to reflect
  # docker-compose down -v
  mysql:
    container_name: mysql
    image: mysql:8.0.29
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: identity
      MYSQL_USER: keycloak
      MYSQL_PASSWORD: keycloak
      MYSQL_ROOT_PASSWORD: root
    volumes:
      - mysql-data:/var/lib/mysql

  # access url - http://localhost:1010/
  keycloak:
    image: quay.io/keycloak/keycloak:21.0
    #image: jboss/keycloak  (Does not support ARM 64 image)
    command: ["start-dev"]
    ports:
      - 1010:8080
      - 1011:8443
    environment:
      KC_HEALTH_ENABLED: true
      KC_METRICS_ENABLED: true
      KC_DB: mysql
      KC_DB_URL: jdbc:mysql://mysql:3306/identity?useSSL=false&allowPublicKeyRetrieval=true&cacheServerConfiguration=true&createDatabaseIfNotExist=true
      KC_DB_USERNAME: root
      KC_DB_PASSWORD: root
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KEYCLOAK_FRONTEND_URL: http://localhost:1010/auth
    volumes:
      #- ./data:/opt/jboss/keycloak/standalone/data
      #- ./themes:/opt/jboss/keycloak/standalone/themes
      #- ./config:/opt/jboss/keycloak/standalone/configuration
      - ./log:/opt/jboss/keycloak/standalone/log
    depends_on:
        - mysql

volumes:
  mysql-data:
    driver: local

networks:
  default:
    name: company_default
ree

Realm settings attached below for the reference.



pom.xml (nimbus-jose-jwt dependency)

        <!-- nimbus-jose-jwt -->
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>9.30.1</version>
        </dependency>

Application.java

package com.company.project;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.util.X509CertUtils;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

@Slf4j
@SpringBootApplication
public class Application {
    public static final String CERTS_ENDPOINT = "http://localhost:1010/realms/employee/protocol/openid-connect/certs";
    public static final String VALID_ID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqaEtlM0RGc2ZzOWJMbjl3NVYzQUc0Sm5UMDJYMW0zaEpMbWY3dmR3SDhJIn0.eyJleHAiOjE2ODc3ODIxMDEsImlhdCI6MTY4Nzc4MTgwMSwiYXV0aF90aW1lIjowLCJqdGkiOiI0OWE0YzIwNC03NDA2LTQ2YTQtYjIwZi0xYmNlZjg0MTg5YjYiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEwMTAvcmVhbG1zL2VtcGxveWVlIiwiYXVkIjoiZW1wbG95ZWUtc2VydmljZS1jbGllbnQiLCJzdWIiOiJmMzMxNGVhNy00NDE4LTRiZjctOWZhNy1jYzVkZWZkZTA5MWIiLCJ0eXAiOiJJRCIsImF6cCI6ImVtcGxveWVlLXNlcnZpY2UtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6Ijg4ZDVmNTQ5LTc5YjUtNDlkOC1iYTY5LTliNDMyNzIxNDk3ZCIsImF0X2hhc2giOiJTRVZtX2Q4Nk5oR2Y4TmVvSWxWUWdRIiwiYWNyIjoiMSIsInNpZCI6Ijg4ZDVmNTQ5LTc5YjUtNDlkOC1iYTY5LTliNDMyNzIxNDk3ZCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyMSIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidXNlcjFAdGVzdC5jb20ifQ.HHdm6clyKB6Lh_iUxKyL6A2zqo3WNOOLBhjKWAeR5OvABK8zjAfAESt-wjV99Br3V5eOiGa2MB1PbMOIKwHW05-ZIDBQ_8SY8WWorIEOGS3BdZCjh-_SsXurQkZtHrKpR8268b6nRBkVT83KX_qd8BIJAA_6vgqwdb6z5Pnt9QzZAuDeDQLia1Ba0kdIua0OU1XoDVpAlxNdeSyHcjbRlbFxHx7nZQKmu3LFsAji8j-ypsp1ts06Jn9LDMhp30tgVKUH1MzpwOvIpD2jlpo6MMgAUmjV6Vy6xJBg46F8LItxXkIyvtRzkJiT4bm2Jubvlr5F2X0t6THY_T6ZopTTlQ";
    public static final String FORGED_ID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImpoS2UzREZzZnM5YkxuOXc1VjNBRzRKblQwMlgxbTNoSkxtZjd2ZHdIOEkifQ.eyJleHAiOjE2ODc3ODIxMDEsImlhdCI6MTY4Nzc4MTgwMSwiYXV0aF90aW1lIjowLCJqdGkiOiI0OWE0YzIwNC03NDA2LTQ2YTQtYjIwZi0xYmNlZjg0MTg5YjYiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjEwMTAvcmVhbG1zL2VtcGxveWVlIiwiYXVkIjoiZW1wbG95ZWUtc2VydmljZS1jbGllbnQiLCJzdWIiOiJmMzMxNGVhNy00NDE4LTRiZjctOWZhNy1jYzVkZWZkZTA5MWIiLCJ0eXAiOiJJRCIsImF6cCI6ImVtcGxveWVlLXNlcnZpY2UtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6Ijg4ZDVmNTQ5LTc5YjUtNDlkOC1iYTY5LTliNDMyNzIxNDk3ZCIsImF0X2hhc2giOiJTRVZtX2Q4Nk5oR2Y4TmVvSWxWUWdRIiwiYWNyIjoiMSIsInNpZCI6Ijg4ZDVmNTQ5LTc5YjUtNDlkOC1iYTY5LTliNDMyNzIxNDk3ZCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyMiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidXNlcjJAdGVzdC5jb20ifQ.KzV8RT8iumpFnPzt6J1HbQw5E-i4ldEzatHETf3bWyOs3GK_TIfMINnMY6Py5TkFnEwRRwLsDlWA2Vhyu70GbMusIZlY3YbmuioqLZ4R7QwE4wrcWjs-NgBAnEaTd-6T-hL3TKvjU9Yyn4WcsXcszgq8QzE9Udh1umDt57wQgWBGPuc8knf_1lh6bqnUmmH7gaEt8Yvw_yZrXxqBmxOiCBwMb4VxeLnvxFvZxYvXQzXPybd1Q25NT0D2loYx4P_1y2mCJSet2qhek0gfeaeJ7BUY66R6gd5fjAj1d7PkmI7YbJYlsLsrCZKZNg76MVf14f0Ck_Ts9Skd6DkW4l1XTA";
    public static final String SIGNATURE_VERIFIED = "Signature verified";
    public static final String SIGNATURE_INVALID = "Invalid signature";
    public static final String PARSE_EXCEPTION = "Invalid Token";
    public static final String EMAIL_CLAIM = "email";

    public static void main(String[] args) throws ParseException, JOSEException {
        SpringApplication.run(Application.class, args);

        // Get the Certificate API Response
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> certsResponse = restTemplate.getForEntity(CERTS_ENDPOINT, String.class);

        // Parse the String JSON response to extract the first key object
        JWKSet jwkSet = JWKSet.parse(certsResponse.getBody());
        RSAKey rsaKey = (RSAKey) jwkSet.getKeys().get(1);

        log.info("Extracted Public Key/Certificate Details");
        log.info("Key: {}", rsaKey);

        // Extract the public key from the RSAKey
        X509Certificate certificate = X509CertUtils.parse(rsaKey.getX509CertChain().get(0).decode());
        RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();

        //Validate Valid ID Token
        verifySignature(publicKey, VALID_ID_TOKEN);

        //Validate Forged ID Token
        verifySignature(publicKey, FORGED_ID_TOKEN);
    }

    public static void verifySignature(RSAPublicKey publicKey, String token) throws ParseException, JOSEException {

        log.info("Validating Token: {}", token);

        // Get the JWSVerifier with given public key
        JWSVerifier verifier = new RSASSAVerifier(publicKey);

        // Parse the JWT token string
        SignedJWT signedJWT;
        signedJWT = SignedJWT.parse(token);

        // Verify the signature
        if (signedJWT.verify(verifier)) {
            log.info(SIGNATURE_VERIFIED);
            JWTClaimsSet claims = signedJWT.getJWTClaimsSet();

            // Extract Claims
            log.info("Email: {}", claims.getClaim(EMAIL_CLAIM));
        } else {
            log.info(SIGNATURE_INVALID);
        }
    }
}

Output

ree


ree

ree




Thank you for taking the time to read this post. I hope that you found it informative and useful in your own development work.

Comments


bottom of page