For the initial lab setup, we can spin up a keycloak server with docker.
https://www.keycloak.org/getting-started/getting-started-docker
docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.5.3 start-devIn order to test the Oauth 2.0 Flow, we need to setup a client.
Client ID is important, it's like a identifier and whatever action we are performing is linked to the client id. We will be using client_id in all the subsequent request later.
Important piece of the information on this image is the redirect url which needs to be set. When we hit the /auth endpoint to get the code and we can use the code to exchange the token. The code is one time use. We will see the flow later.
Oauth was primiraly build for the authorization but the disruption of Oauth was so heavy on the starting years and compaies were exploiting it for the authentication building their own layers on top of OAuth. Due to this scattered and ambigious implementation leads to developement of the OIDC which expans the OAuth and formalize the use as a authentication in a standard way.
That's enough background. Let's see the action.
Keeping this simple for now. Let's create a user without MFA or any overhead. I would do a MFA part on seperate post.
After the user is created we can setup credentials for the user which we will be using in a login process.
That's all needed to get started.
The starting point for the implementation is a /auth endpoint
Let's do the flow on the browser.
We got these as a response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJQ041R1Rla1Y5akZKS0wxUXA0OXNNbS1XMW8wTEU0c0NGdGZwV1VkTU0wIn0.eyJleHAiOjE3NzEwMzc1MzUsImlhdCI6MTc3MTAzNzIzNSwiYXV0aF90aW1lIjoxNzcxMDM3MjE5LCJqdGkiOiJvbnJ0YWM6ZTAzNjc4NDctODA4My0yYjBlLWQ2NzQtNDMwYmViZWFmOWE5IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9taW5pbGFiIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjU3MjRlNzNmLTlkZWYtNDEzNS1hNTczLWNkMGM4ZDc4NDUwZiIsInR5cCI6IkJlYXJlciIsImF6cCI6IndlYi1hcHAiLCJzaWQiOiJlMXRyenliaTFyX1NYTmM1YW1QUFk2WloiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJkZWZhdWx0LXJvbGVzLW1pbmlsYWIiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJUZXN0IFVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0dXNlciIsImdpdmVuX25hbWUiOiJUZXN0IiwiZmFtaWx5X25hbWUiOiJVc2VyIiwiZW1haWwiOiJ0ZXN0QHVzZXIuaW8ifQ.ZK26i7KS7GXIUVX27H4wvI8JDYlhSMP0meUO3_LOnKTGPamlQe6Z0NzQz7FhHK6KjrExHLu9JGcl4J4udYfy0DZEO6ENL4L3M_jLgJ4J1-RJxHg2o1xd0R9FAWR31GLKZtFdVmwZP71MUjFQveIQftwmPAUgqzmKjpycZ8Npfj9YIuG2c3n52sn28KTwITspDuFVdKD-M0UBbr1kmkPDHrXHjPXwAV9LlTA9ETV8dZ5CmASeSFLnK8Z2RVBwZiCj3WSe9nvryMbGUaG21IzhstDxrkFh-nZK4w3MTAZx-OhwGJK3b9CT-47VxmFpOd3UbYDAlHr8BFb9c2W5IJEsfw",
"expires_in": 299,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiZGEyOWY3Yy03MDc4LTQzOTEtODcyNi1mOTVkY2NlZjViMGUifQ.eyJleHAiOjE3NzEwMzkwMzYsImlhdCI6MTc3MTAzNzIzNiwianRpIjoiNjA1ZDAyNGMtYWE0Ny1jYTA2LWMyNDctZTRjNjBhMzliOGUxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9taW5pbGFiIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9taW5pbGFiIiwic3ViIjoiNTcyNGU3M2YtOWRlZi00MTM1LWE1NzMtY2QwYzhkNzg0NTBmIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6IndlYi1hcHAiLCJzaWQiOiJlMXRyenliaTFyX1NYTmM1YW1QUFk2WloiLCJzY29wZSI6Im9wZW5pZCBhY3IgcHJvZmlsZSByb2xlcyBiYXNpYyBlbWFpbCB3ZWItb3JpZ2lucyJ9.qpdNG_seIQ23IE6nJ6-9f-fo3sQMeaBv4BuzdJKCSkeH8xCo9_suSkAfWL1PBHFqPxU3RzsM3IZNNPrZU3Drsg",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJQ041R1Rla1Y5akZKS0wxUXA0OXNNbS1XMW8wTEU0c0NGdGZwV1VkTU0wIn0.eyJleHAiOjE3NzEwMzc1MzUsImlhdCI6MTc3MTAzNzIzNiwiYXV0aF90aW1lIjoxNzcxMDM3MjE5LCJqdGkiOiIwOGIwNWZjZS0zMmM3LTYyNzUtMmQ2Yi0wOGNkMWQ0ZTcyMTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL21pbmlsYWIiLCJhdWQiOiJ3ZWItYXBwIiwic3ViIjoiNTcyNGU3M2YtOWRlZi00MTM1LWE1NzMtY2QwYzhkNzg0NTBmIiwidHlwIjoiSUQiLCJhenAiOiJ3ZWItYXBwIiwic2lkIjoiZTF0cnp5Ymkxcl9TWE5jNWFtUFBZNlpaIiwiYXRfaGFzaCI6InBFbWVmcTBEdzFuUnNTNWpnSFA4TEEiLCJhY3IiOiIxIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiVGVzdCBVc2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdHVzZXIiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciIsImVtYWlsIjoidGVzdEB1c2VyLmlvIn0.eqFKT_p4Ge35CEDn-P-kD9mMTtO5nq0XAsvGK578Ca2t9jVhE-Gjjaa0CaM2XAIl79Q0HfGy9NJQdGO5VFb6xiI-XxZr4YyOcohsAl8dEJJuNMCPp4RyQem8kgvqzckBl-s6nONHrtXYKeWQ3K5xLUd-g63HWW2o928j9jxQNXXDNdHT4mpnbvpXOsCTcp3Y4GfFQojT32wo_N_RG0nT9iqclt5cSNDhYIM_penjIJIk_xwSZURWibUFhvzYHqgXcrgjd56LibQaRk7NJEroxqYeEXwYYDoFVPHfzJS3niM3enQXFQ4epbfhWz8VS-K2tnLX9jWHbYjfi6ijQnTe5w",
"not-before-policy": 0,
"session_state": "e1trzybi1r_SXNc5amPPY6ZZ",
"scope": "openid profile email"
}Let's elobarate the access_token in jwt.io
{
"exp": 1771037535,
"iat": 1771037235,
"auth_time": 1771037219,
"jti": "onrtac:e0367847-8083-2b0e-d674-430bebeaf9a9",
"iss": "http://localhost:8080/realms/minilab",
"aud": "account",
"sub": "5724e73f-9def-4135-a573-cd0c8d78450f",
"typ": "Bearer",
"azp": "web-app",
"sid": "e1trzybi1r_SXNc5amPPY6ZZ",
"acr": "1",
"allowed-origins": [
"http://localhost:3000"
],
"realm_access": {
"roles": [
"offline_access",
"default-roles-minilab",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid profile email",
"email_verified": false,
"name": "Test User",
"preferred_username": "testuser",
"given_name": "Test",
"family_name": "User",
"email": "test@user.io"
}Referesh Token
{
"exp": 1771039036,
"iat": 1771037236,
"jti": "605d024c-aa47-ca06-c247-e4c60a39b8e1",
"iss": "http://localhost:8080/realms/minilab",
"aud": "http://localhost:8080/realms/minilab",
"sub": "5724e73f-9def-4135-a573-cd0c8d78450f",
"typ": "Refresh",
"azp": "web-app",
"sid": "e1trzybi1r_SXNc5amPPY6ZZ",
"scope": "openid acr profile roles basic email web-origins"
}ID Token
{
"exp": 1771037535,
"iat": 1771037236,
"auth_time": 1771037219,
"jti": "08b05fce-32c7-6275-2d6b-08cd1d4e7219",
"iss": "http://localhost:8080/realms/minilab",
"aud": "web-app",
"sub": "5724e73f-9def-4135-a573-cd0c8d78450f",
"typ": "ID",
"azp": "web-app",
"sid": "e1trzybi1r_SXNc5amPPY6ZZ",
"at_hash": "pEmefq0Dw1nRsS5jgHP8LA",
"acr": "1",
"email_verified": false,
"name": "Test User",
"preferred_username": "testuser",
"given_name": "Test",
"family_name": "User",
"email": "test@user.io"
}So far we have user identity credentials auth code to get the token.
Now, we are implementing the heart of the OAuth Delegation or authorization. We need to create a client_scope.
These client scopes are global to the realm meaning it can be attached to any available clients on the realm. To effective impose those scope we need to add that on the clients using
That's all we need to worry about for now to make the consent workflow working.
I have built a minimal node.js sever to simulate the transfer money mocked function to show the consent workflow.








