PolarSPARC

Understanding OAuth2 and OpenID Connect


Bhaskar S 09/05/2020


Overview

There seems to be a lot of confusion around OAuth2 and OpenID Connect (OIDC for short). In this article, we will go on a journey to understand and clarify what OAuth2 and OIDC really are.

Alice is hosting a party and wants to send an invite to a select group of friends. She plans to send a digital invitation via a site called KoolInvitez. The site needs a list of emails to send out the invitation. Alice stores all her contacts (as a collection of groups) in a site called EzGroupContacts. How should Alice provide access to the group called 'CloseFriends' in EzGroupContacts to KoolInvitez to send the invites ??? Should Alice provide her credentials for EzGroupContacts to KoolInvitez so it can access the contacts in the group 'CloseFriends' ??? This is a security anti-pattern and *NOT* recommended. This is where OAuth2 comes in handy. Think of OAuth2 as a way of handing someone a Valet key, who will have limited access to perform their task. In other words, OAuth2 is an open standard for users to grant access to their information on a site or application to another site, but without revealing their credentials.

OIDC, on the other hand, is an extension on top of OAuth2, that is used to verify the identify of a user (authentication) in a standard way. As an example, there are many sites that do not have any user registration and rely on Google or Facebook for identity verification (authentication).

Before we go any further, let us define some terms that will be useful in the context of OAuth2 and OIDC as follows:

Term Description
Resource Data or information that a user owns (Ex: the contact list 'CloseFriends')
Resource Owner The owner of a Resource (Ex: Alice)
Resource Server The server where the Resource is hosted (Ex: EzGroupContacts)
Client An application or a site that needs access to a user Resource (Ex: KoolInvitez)
Authorization Server The OAuth2/ODIC server where a user grants a consent to a Client, to access their Resource(s)
Access Token A security token which the Client can present to the Resource Server to get access to a user's Resource
Front Channel Requests coming from a user agent (such as a web browser) to a server
Back Channel Requests coming from a server to another server. This is done for security reasons

The basic *FUNDAMENTAL* flow of OAuth2 is referred to as the Authorization Code flow and all the other flows are variations of this basic flow. The Authorization Code flow works as follows:

The following diagram illustrates the basic Authorization Code flow:


Basic Flow
Figure.1

NOTE :: In some cases, the Authorization Server and the Resource Server may be ONE.

Installation and Setup

The installation will be on a Ubuntu 20.04 LTS based Linux desktop.

Ensure Docker is installed on the system. Else, follow the instructions provided in the article Introduction to Docker to complete the installation.

There are few open source implementations of the Authorization Server that implement the OAuth2/OIDC standards as follows:

For our setup and demonstration, we will use the Keycloak Authorization Server in conjuction with the PostgreSQL database.

Check the latest stable version for Postgres docker image. Version 12.4 was the latest at the time of this article.

To download the latest docker image for Postgres, execute the following command:

$ docker pull postgres:12.4

The following would be a typical output:

Output.1

12.4: Pulling from library/postgres
bf5952930446: Pull complete 
9577476abb00: Pull complete 
2bd105512d5c: Pull complete 
b1cd21c26e81: Pull complete 
34a7c86cf8fc: Pull complete 
274e7b0c38d5: Pull complete 
3e831b350d37: Pull complete 
38fa0d496534: Pull complete 
c989da35e5c0: Pull complete 
26dc6fdd7b2d: Pull complete 
3c5032512cf3: Pull complete 
26910ececf99: Pull complete 
0339413523e8: Pull complete 
d61df7db53da: Pull complete 
Digest: sha256:9f325740426d14a92f71013796d98a50fe385da64a7c5b6b753d0705add05a21
Status: Downloaded newer image for postgres:12.4
docker.io/library/postgres:12.4

Check the latest stable version for Keycloak docker image. Version 11.0.1 was the latest at the time of this article.

To download the latest docker image for Keycloak, execute the following command:

$ docker pull jboss/keycloak:11.0.1

The following would be a typical output:

Output.2

11.0.1: Pulling from jboss/keycloak
41ae95b593e0: Pull complete 
f20f68829d13: Pull complete 
6c304f8c55f0: Pull complete 
7db1d4725e09: Pull complete 
59cf4c955406: Pull complete 
Digest: sha256:d7cb114cdb5dcc4b2c9b56ac7b827249596491db02bb82177edb42413056aa13
Status: Downloaded newer image for jboss/keycloak:11.0.1
docker.io/jboss/keycloak:11.0.1

To create a new bridge network called my-iam-net, execute the following command:

$ docker network create my-iam-net

The following would be a typical output:

Output.3

64852330d28ade21943e0d8acfcf91fe4037c4f378d40c97f5b902e70674003d

We will store the database files in the directory $HOME/Downloads/DATA/postgres on the host.

To start the Postgres database, execute the following command:

$ docker run -d --rm --name postgres --net my-iam-net -e POSTGRES_DB=keycloak -e POSTGRES_USER=keycloak -e POSTGRES_PASSWORD=keycloak\$123 -p 5432:5432 -v $HOME/Downloads/DATA/postgres:/var/lib/postgresql/data postgres:12.4

The following would be a typical output:

Output.4

26e000434238aea7a1f6cace9be81d243a3a37f6ab7cfdef3625ed03b0e296f1

To check the Postgres database logs, execute the following command:

$ docker logs postgres

The following would be a typical output:

Output.5

...
...
...
PostgreSQL init process complete; ready for start up.

2020-08-29 17:07:00.296 UTC [1] LOG:  starting PostgreSQL 12.4 (Debian 12.4-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
2020-08-29 17:07:00.296 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2020-08-29 17:07:00.296 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2020-08-29 17:07:00.299 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2020-08-29 17:07:00.316 UTC [65] LOG:  database system was shut down at 2020-08-29 17:07:00 UTC
2020-08-29 17:07:00.321 UTC [1] LOG:  database system is ready to accept connections

!!! ATTENTION !!!

Logs in the article have been trimmed to just show the pieces relevant to the context
The lines with three dots '...' indicates code that has been truncated

To start the Keycloak OAuth2/ODIC server, execute the following command:

$ docker run --name keycloak --net my-iam-net -e DB_VENDOR=POSTGRES -e DB_DATABASE=keycloak -e DB_SCHEMA=public -e DB_ADDR=postgres -e DB_PORT=5432 -e DB_USER=keycloak -e DB_PASSWORD=keycloak\$123 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=kc_admin\$123 -p 8080:8080 jboss/keycloak:11.0.1

The following would be a typical output:

Output.6

Added 'admin' to '/opt/jboss/keycloak/standalone/configuration/keycloak-add-user.json', restart server to load user
-b 0.0.0.0
=========================================================================

  Using PostgreSQL database

=========================================================================

17:12:51,436 INFO  [org.jboss.modules] (CLI command executor) JBoss Modules version 1.10.1.Final
17:12:51,486 INFO  [org.jboss.msc] (CLI command executor) JBoss MSC version 1.4.11.Final
17:12:51,494 INFO  [org.jboss.threads] (CLI command executor) JBoss Threads version 2.3.3.Final
17:12:51,585 INFO  [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: Keycloak 11.0.1 (WildFly Core 12.0.3.Final) starting
17:12:51,668 INFO  [org.jboss.vfs] (MSC service thread 1-7) VFS000002: Failed to clean existing content for temp file provider of type temp. Enable DEBUG level log to find what caused this
...
...
...
The batch executed successfully
17:12:55,936 INFO  [org.jboss.as] (MSC service thread 1-3) WFLYSRV0050: Keycloak 11.0.1 (WildFly Core 12.0.3.Final) stopped in 14ms
=========================================================================

  JBoss Bootstrap Environment

  JBOSS_HOME: /opt/jboss/keycloak

  JAVA: java

  JAVA_OPTS:  -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true  --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED --add-exports=jdk.unsupported/sun.reflect=ALL-UNNAMED

=========================================================================

17:12:56,646 INFO  [org.jboss.modules] (main) JBoss Modules version 1.10.1.Final
17:12:57,052 INFO  [org.jboss.msc] (main) JBoss MSC version 1.4.11.Final
17:12:57,059 INFO  [org.jboss.threads] (main) JBoss Threads version 2.3.3.Final
17:12:57,151 INFO  [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: Keycloak 11.0.1 (WildFly Core 12.0.3.Final) starting
17:12:57,258 INFO  [org.jboss.vfs] (MSC service thread 1-4) VFS000002: Failed to clean existing content for temp file provider of type temp. Enable DEBUG level log to find what caused this
...
...
...
17:13:12,265 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 65) WFLYUT0021: Registered web context: '/auth' for server 'default-server'
17:13:12,334 INFO  [org.jboss.as.server] (ServerService Thread Pool -- 46) WFLYSRV0010: Deployed "keycloak-server.war" (runtime-name : "keycloak-server.war")
17:13:12,381 INFO  [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0212: Resuming server
17:13:12,384 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 11.0.1 (WildFly Core 12.0.3.Final) started in 16060ms - Started 687 of 992 services (703 services are lazy, passive or on-demand)
17:13:12,385 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
17:13:12,386 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990

!!! CAUTION !!!

Missing any of the options will result in the following exception:
17:10:55,400 ERROR [org.jboss.as.controller.management-operation] (Controller Boot Thread) WFLYCTL0013: Operation ("add") failed - address: ([("subsystem" => "microprofile-metrics-smallrye")]): java.lang.NullPointerException

Rather than execute the two docker commands individually, one can use the following docker-compose file:

postgres-keycloak.yml
version: '3'

networks:
    default:
        external:
            name: my-iam-net

services:
  postgres:
    container_name: postgres
    image: postgres:12.4
    volumes:
      - ${HOME}/Downloads/DATA/postgres:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=keycloak
      - POSTGRES_USER=keycloak
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    ports:
      - 5432:5432
  keycloak:
    container_name: keycloak
    image: jboss/keycloak:11.0.1
    environment:
      - DB_VENDOR=POSTGRES
      - DB_DATABASE=keycloak
      - DB_SCHEMA=public
      - DB_ADDR=postgres
      - DB_PORT=5432
      - DB_USER=keycloak
      - DB_PASSWORD=${POSTGRES_PASSWORD}
      - KEYCLOAK_USER=admin
      - KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD}
    ports:
      - 8080:8080
    depends_on:
      - postgres

The variables POSTGRES_PASSWORD and KEYCLOAK_PASSWORD are defined in a file called vars.env as follows:

vars.env
POSTGRES_PASSWORD=keycloak$123
KEYCLOAK_PASSWORD=kc_admin$123

Launch a web browser and open the URL http://localhost:8080/auth/admin. It will prompt us with a login screen as shown in the following illustration:


Login Screen
Figure.2

Enter the username of admin and password of kc_admin$123 and click on the Log In button as shown in the illustration below:


Login Submit
Figure.3

On successful login, it will take us to the Master realm page as shown in the illustration below:


Master Realm
Figure.4

A Realm is like a namespace where all the instance setup objects reside. We need to create a new realm for our setup. To create a new realm click on Master dropdown (on the top left-hand corner) and click on the Add realm button as shown in the illustration below:


Add Realm
Figure.5

We will create a new realm called Testing for our setup and click on the Create button as shown in the illustration below:


Testing Realm
Figure.6

On successful creation, it will take us to the Testing realm page, where we enter the Display name, leave the remaining options as is, and click on the Save button as shown in the illustration below:


Testing Realm Display Name
Figure.7

On the Testing realm page, click on OpenID Endpoint Configuration as shown in the illustration below:


Endpoint Configuration
Figure.8

This will take us to the Endpoint Configuration page, where we can make a note of the various URL endpoints for our demonstration later, as shown in the illustration below:


Endpoint URLs
Figure.9

Every application that interacts with Keycloak needs to be pre-registered. To create a new client, click on the Clients option (on the left-hand side), and the click on the Create button as shown in the illustration below:


New Client
Figure.10

On the Add Client page, we will enter a new Client ID called test-client, enter the Root URL to be http://localhost:5000/, and then click on the Save button as shown in the illustration below:


New Client
Figure.11

On successful creation, it will take us to the Test-client client page, where we enter the Name, Description, Valid Redirect URIs, leave the remaining options as is, and click on the Save button as shown in the illustration below:


Test-client Client
Figure.12

In Figure.12 above, the option Standard Flow Enabled (enclosed within a rectangle) is what enables the OAuth2 flow.

We need to make a note of the secret token associated with the just created client test-client. To do that, click on the Clients option (on the left-hand side) and then click on the client ID test-client from the list of clients as shown in the illustration below:


List of Clients
Figure.13

This will take us to the Test-client client page. Click on the Credentials tab, and make a note of the client Secret as shown in the illustration below:


Client Secret
Figure.14

For the OAuth2 flow to work in Keycloak, we need to pre-registered a user (Resource Owner). To create a new user, click on the Users option (on the left-hand side), and the click on the Add user button (top right-hand corner) as shown in the illustration below:


Add New User
Figure.15

On successful creation, it will take us to the Add user page, where we enter the Username called test-user, with an Email of test-user@localhost, with a First Name of Test, with a Last Name of User, leave the remaining options as is, and click on the Save button as shown in the illustration below:


Save New User
Figure.16

Next, we need to set a user credential (password) for the newly created user test-user. To set the user password, click on the Credentials tab (circled 1), enter a Password of test-user$123, re-enter the same password in Confirmation, turn OFF the option Temporary (circled 2), and click on the Set password button (circled 3) as shown in the illustration below:


Set User Password
Figure.17

This completes the installation and setup for the demonstration of the OAuth2 flow.

Hands-on OAuth2 Authorization Code Flow

We will use Python to implement the Client (as a standalone webserver) and display the data elements of the OAuth2 Authorization Code flow on a simple HTML page.

The following is the directory structure for the Client source code:


Dir Structure
Figure.18

The following is the listing of the HTML file for the Authorization Code flow:

index.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Understanding OAuth2</title>
        <link rel="stylesheet" href="../static/css/bootstrap.min.css">
        <link rel="shortcut icon" href="../static/images/favicon.ico">
        <style>
            td {
              white-space: normal !important;
              word-wrap: break-word;
            }
            table {
              table-layout: fixed;
            }
        </style>
    </head>
    <body class="bg-light">
        <div class="container">
            <br/>
            <div class="display-4 text-center text-secondary">Understanding OAuth2</div>
            <br/>
            <hr/>
            <nav class="nav nav-tabs nav-justified">
                <a id="code" class="nav-item nav-link" href="http://localhost:5000/auth_code">Authorization Code</a>
                <a id="token" class="nav-item nav-link" href="http://localhost:5000/access_token">Access Token</a>
                <a id="profile" class="nav-item nav-link" href="http://localhost:5000/user_profile">User Profile</a>
                <a id="logout" class="nav-item nav-link" href="http://localhost:5000/logout">Logout</a>
            </nav>
            <br/>
            <table class="table table-striped table-bordered">
                <thead>
                    <tr class="bg-secondary text-light">
                        <th width="20%">Key</th>
                        <th width="80%">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>State</td>
                        <td>{{ data['state'] }}</td>
                    </tr>
                    <tr>
                        <td>Authorization Code</td>
                        <td id="a_code">{{ data['code'] }}</td>
                    </tr>
                    <tr>
                        <td>Session</td>
                        <td>{{ data['session'] }}</td>
                    </tr>
                    <tr>
                        <td>Access Token</td>
                        <td>{{ data['a_token'] }}</td>
                    </tr>
                    <tr>
                        <td>Refresh Token</td>
                        <td>{{ data['r_token'] }}</td>
                    </tr>
                    <tr>
                        <td>Token Type</td>
                        <td>{{ data['t_type'] }}</td>
                    </tr>
                    <tr>
                        <td>Scope</td>
                        <td>{{ data['scope'] }}</td>
                    </tr>
                    <tr>
                        <td>User Email</td>
                        <td>{{ data['email'] }}</td>
                    </tr>
                </tbody>
            </table>
        </div>
        <script src="../static/js/jquery.slim.min.js"></script>
        <script src="../static/js/bootstrap.bundle.min.js"></script>
    </body>
</html>

The following is the listing of the Python Client for the Authorization Code flow:

AuthCode.py
###
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   05 Sep 2020
###

import random
import requests
import string
from flask import Flask, jsonify, render_template, redirect, request
from urllib.parse import urlencode

app = Flask(__name__)

OAuthConfig = {
    'authURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/auth?',
    'tokenURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/token',
    'profileURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/userinfo',
    'logoutURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/logout?'
}

oauth = {
    'code': '',
    'state': '',
    'session': '',
    'a_token': '',
    'r_token': '',
    't_type': '',
    'scope': '',
    'email': ''
}


def oauth_init():
    oauth['code'] = ''
    oauth['state'] = random_state()
    oauth['session'] = ''
    oauth['a_token'] = ''
    oauth['r_token'] = ''
    oauth['t_type'] = ''
    oauth['scope'] = ''
    oauth['email'] = ''


def random_state():
    letters_digits = string.ascii_letters + string.digits
    state_str = ''.join((random.choice(letters_digits) for _ in range(20)))
    print(f"State: {state_str}")
    return state_str


@app.route('/')
def login():
    oauth_init()
    return render_template('index.html', data=oauth)


@app.route('/auth_code')
def authenticate():
    params = {'client_id': 'test-client',
              'response_type': 'code',
              'redirect_uri': 'http://localhost:5000/callback',
              'state': oauth['state']}
    query_str = urlencode(params)
    print(f"Ready to redirect to URL: {OAuthConfig['authURL'] + query_str}")
    return redirect(OAuthConfig['authURL'] + query_str, 302)


@app.route('/callback')
def callback():
    oauth['code'] = request.args.get('code')
    oauth['session'] = request.args.get('session_state')
    print(f"Received code: {oauth['code']}, session: {oauth['session']}, state: {request.args.get('state')}")
    return render_template('index.html', data=oauth)


@app.route('/access_token')
def token():
    data = {
        'client_id': 'test-client',
        'grant_type': 'authorization_code',
        'code': oauth['code'],
        'client_secret': 'f7d87a95-604b-4c66-9d60-44c42c91650f',
        'redirect_uri': 'http://localhost:5000/callback'
    }
    res = requests.post(OAuthConfig['tokenURL'], data=data)
    print(f"Status code: {res.status_code}")
    if res.status_code == 200:
        json = res.json()
        print(f"Received response: {json}")
        oauth['a_token'] = json['access_token']
        oauth['r_token'] = json['refresh_token']
        oauth['t_type'] = json['token_type']
        oauth['scope'] = json['scope']
    else:
        oauth['a_token'] = '*** FAILED ***'
    return render_template('index.html', data=oauth)


@app.route('/user_profile')
def profile():
    res = requests.get(OAuthConfig['profileURL'], headers={'Authorization': 'Bearer ' + oauth['a_token']})
    print(f"Status code: {res.status_code}")
    if res.status_code == 200:
        json = res.json()
        print(f"Received response: {json}")
        oauth['email'] = json['email']
    else:
        oauth['email'] = '*** UNKNOWN ***'
    return render_template('index.html', data=oauth)


@app.route('/logout')
def logout():
    params = {'redirect_uri': 'http://localhost:5000/'}
    query_str = urlencode(params)
    return redirect(OAuthConfig['logoutURL'] + query_str)


if __name__ == '__main__':
    app.run(debug=True)

To start the Python Client AuthCode.py, execute the following command:

$ python ./OAuth2/AuthCode.py

The following would be a typical output:

Output.7

* Serving Flask app "AuthCode" (lazy loading)
* Environment: production
  WARNING: This is a development server. Do not use it in a production deployment.
  Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 251-255-341

Now, launch a web browser and open the URL http://localhost:5000. We will land on a page as shown in the illustration below:


OAuth2 Screen
Figure.19

Clicking on the item Authorization Code triggers the execution of the Python method authenticate(), which sends a HTTP redirect request to the URL endpoint http://localhost:8080/auth/realms/testing/protocol/openid-connect/auth on the Keycloak Authorization Server.

The following is the screenshot of the HTTP redirect request captured via Wireshark:


Authenticate Request
Figure.20

Notice the use of request_type=code for the OAuth2 Authorization Code flow.

In our setup, the Keycloak instance acts as both the Authorization Server and the Resource Server. Once the Keycloak Authorization Server receives the authorization code request, it redirects the user (Resource Owner) to authenticate as shown in the illustration below:


User Authentication
Figure.21

Enter the username of test-user and password of test-user$123 and click on the Log In button.

On successful authentication, the Keycloak Authorization Server responds back on the callback URL of the Python Client http://localhost:5000/callback with a valid authorization code.

The following is the screenshot of the HTTP callback response captured via Wireshark:


Callback Response
Figure.22

Notice the authorization code code=835f7a9... being sent to the Client.

The browser now refreshes and we will land on a page as shown in the illustration below:


Authorization Code Screen
Figure.23

Clicking on the item Access Token triggers the execution of the Python method token(), which sends a HTTP POST request (with the authorization code and the Client secret) to the URL endpoint http://localhost:8080/auth/realms/testing/protocol/openid-connect/token on the Keycloak Authorization Server.

The following is the screenshot of the HTTP token request captured via Wireshark:


Token Request
Figure.24

On success, the Keycloak Authorization Server responds back with an Access Token to the Python Client.

The following is the screenshot of the HTTP token response captured via Wireshark:


Token Response
Figure.25

Notice the access_token eyJhbGc... being sent to the Client.

The browser now refreshes and we will land on a page as shown in the illustration below:


Access Token Screen
Figure.26

Clicking on the item User Profile triggers the execution of the Python method profile(), which sends a HTTP request (with the access token in the HTTP Authorization header as a Bearer token) to the URL endpoint http://localhost:8080/auth/realms/testing/protocol/openid-connect/userinfo on the Keycloak Resource Server.

The following is the screenshot of the HTTP user profile request captured via Wireshark:


User Profile Request
Figure.27

On success, the Keycloak Resource Server responds back with the profile information of the user (Resource Owner) which includes their email-id to the Python Client.

The following is the screenshot of the HTTP user profile response captured via Wireshark:


User Profile Response
Figure.28

Notice the email test-user@localhost being sent to the Client.

The browser now refreshes and we will land on a page as shown in the illustration below:


User Email Screen
Figure.29

Clicking on the item Logout triggers the execution of the Python method logout(), which sends a HTTP request to the URL endpoint http://localhost:8080/auth/realms/testing/protocol/openid-connect/logout on the Keycloak Authorization Server to logout and terminate the active user session and invalidating the Access Token.

OAuth2 Implicit Grant Flow

The Implicit Grant flow is a simplified and *LESS* secure version of the OAuth2 flow, primarily targeted for a single-page JavaScript based application, to get an Access Token directly without getting an Authorization Code first and then exchaning for an Access Token. All interactions happen only via the Front Channel. The Implicit Grant flow is vulnerable to Access Token leakage (the token is returned in the URL and will be logged in the web browser's history) and replay attack (malicious reuse of an Access Token by an attacker). The Implicit Grant flow works as follows:

We will *NOT* demonstrate this flow in this article.

OAuth2 Resource Owner Password Flow

The Resource Owner Password flow is targeted for use-cases where the Client is trusted by the user (Resource Owner) or migrating legacy Clients (using direct authentication schemes such as HTTP Basic or Digest) to get an Access Token directly in exchange for the username/password of the Resource Owner without getting an Authorization Code first and then exchaning for an Access Token. All interactions happen only via the Back Channel. The Resource Owner Password flow works as follows:

The Resource Owner Password flow is *NOT* recommended as it uses the user's (Resource Owner) username and password on behalf of the user, which is impersonation (no way to know if the request was initiated by the Resource Owner or an attacker).

The following is the listing of the HTML file for both the Resource Owner Password and Client Credentials flows:

index2.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Understanding OAuth2 ({{ data['flow'] }})</title>
        <link rel="stylesheet" href="../static/css/bootstrap.min.css">
        <link rel="shortcut icon" href="../static/images/favicon.ico">
        <style>
            td {
              white-space: normal !important;
              word-wrap: break-word;
            }
            table {
              table-layout: fixed;
            }
        </style>
    </head>
    <body class="bg-light">
        <div class="container">
            <br/>
            <div class="display-4 text-center text-secondary">Understanding OAuth2 ({{ data['flow'] }})</div>
            <br/>
            <hr/>
            <nav class="nav nav-tabs nav-justified">
                <a id="token" class="nav-item nav-link" href="http://localhost:5000/access_token">Access Token</a>
                <a id="profile" class="nav-item nav-link" href="http://localhost:5000/user_profile">User Profile</a>
                <a id="logout" class="nav-item nav-link" href="http://localhost:5000/logout">Logout</a>
            </nav>
            <br/>
            <table class="table table-striped table-bordered">
                <thead>
                    <tr class="bg-secondary text-light">
                        <th width="20%">Key</th>
                        <th width="80%">Value</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>Session</td>
                        <td>{{ data['session'] }}</td>
                    </tr>
                    <tr>
                        <td>Access Token</td>
                        <td>{{ data['a_token'] }}</td>
                    </tr>
                    <tr>
                        <td>Refresh Token</td>
                        <td>{{ data['r_token'] }}</td>
                    </tr>
                    <tr>
                        <td>Token Type</td>
                        <td>{{ data['t_type'] }}</td>
                    </tr>
                    <tr>
                        <td>Scope</td>
                        <td>{{ data['scope'] }}</td>
                    </tr>
                    <tr>
                        <td>User Email</td>
                        <td>{{ data['email'] }}</td>
                    </tr>
                </tbody>
            </table>
        </div>
        <script src="../static/js/jquery.slim.min.js"></script>
        <script src="../static/js/bootstrap.bundle.min.js"></script>
    </body>
</html>

The following is the listing of the Python Client for the Resource Owner Password flows:

OwnerPass.py
###
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   05 Sep 2020
###

import requests
from flask import Flask, render_template, redirect
from urllib.parse import urlencode

app = Flask(__name__)

OAuthConfig = {
    'tokenURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/token',
    'profileURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/userinfo',
    'logoutURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/logout?'
}

oauth = {
    'flow': 'Resource Owner',
    'session': '',
    'a_token': '',
    'r_token': '',
    't_type': '',
    'scope': '',
    'email': ''
}


def oauth_init():
    oauth['session'] = ''
    oauth['a_token'] = ''
    oauth['r_token'] = ''
    oauth['t_type'] = ''
    oauth['scope'] = ''
    oauth['email'] = ''


@app.route('/')
def login():
    oauth_init()
    return render_template('index2.html', data=oauth)


@app.route('/access_token')
def token():
    data = {
        'client_id': 'test-client',
        'client_secret': 'f7d87a95-604b-4c66-9d60-44c42c91650f',
        'grant_type': 'password',
        'username': 'test-user',
        'password': 'test-user$123'
    }
    res = requests.post(OAuthConfig['tokenURL'], data=data)
    print(f"Status code: {res.status_code}")
    if res.status_code == 200:
        json = res.json()
        print(f"Received response: {json}")
        oauth['session'] = json['session_state']
        oauth['a_token'] = json['access_token']
        oauth['r_token'] = json['refresh_token']
        oauth['t_type'] = json['token_type']
        oauth['scope'] = json['scope']
    else:
        oauth['a_token'] = '*** FAILED ***'
    return render_template('index2.html', data=oauth)


@app.route('/user_profile')
def profile():
    res = requests.get(OAuthConfig['profileURL'], headers={'Authorization': 'Bearer ' + oauth['a_token']})
    print(f"Status code: {res.status_code}")
    if res.status_code == 200:
        json = res.json()
        print(f"Received response: {json}")
        oauth['email'] = json['email']
    else:
        oauth['email'] = '*** UNKNOWN ***'
    return render_template('index2.html', data=oauth)


@app.route('/logout')
def logout():
    params = {'redirect_uri': 'http://localhost:5000/'}
    query_str = urlencode(params)
    return redirect(OAuthConfig['logoutURL'] + query_str)


if __name__ == '__main__':
    app.run(debug=True)

Stop the Python Client AuthCode.py if is already running, and start the Python Client OwnerPass.py to see this flow in action.

The following illustration shows the browser page after going through the items Access Token and User Profile:


Owner Password Flow
Figure.30

OAuth2 Client Credentials Flow

The Client Credentials flow is targeted for use-cases where the Client is a service as well as a Resource Owner and wants to get an Access Token to access its own Resource. All interactions happen only via the Back Channel. The Client Credentials flow works as follows:

Before we proceed, we need to make a small change in the Test-client client page. We need to enable the option Service Accounts Enabled and click on the Save button as shown in the illustration below:


Enable Service Account
Figure.31

The following is the listing of the Python Client for the Client Credentials flow:

ClientCredential.py
###
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   05 Sep 2020
###

import requests
from flask import Flask, render_template, redirect
from urllib.parse import urlencode

app = Flask(__name__)

OAuthConfig = {
    'tokenURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/token',
    'profileURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/userinfo',
    'logoutURL': 'http://localhost:8080/auth/realms/testing/protocol/openid-connect/logout?'
}

oauth = {
    'flow': 'Client Credentials',
    'session': '',
    'a_token': '',
    'r_token': '',
    't_type': '',
    'scope': '',
    'email': ''
}


def oauth_init():
    oauth['session'] = ''
    oauth['a_token'] = ''
    oauth['r_token'] = ''
    oauth['t_type'] = ''
    oauth['scope'] = ''
    oauth['email'] = ''


@app.route('/')
def login():
    oauth_init()
    return render_template('index2.html', data=oauth)


@app.route('/access_token')
def token():
    data = {
        'client_id': 'test-client',
        'client_secret': 'f7d87a95-604b-4c66-9d60-44c42c91650f',
        'grant_type': 'client_credentials'
    }
    res = requests.post(OAuthConfig['tokenURL'], data=data)
    print(f"Status code: {res.status_code}")
    if res.status_code == 200:
        json = res.json()
        print(f"Received response: {json}")
        oauth['session'] = json['session_state']
        oauth['a_token'] = json['access_token']
        oauth['r_token'] = json['refresh_token']
        oauth['t_type'] = json['token_type']
        oauth['scope'] = json['scope']
    else:
        oauth['a_token'] = '*** FAILED ***'
    return render_template('index2.html', data=oauth)


@app.route('/user_profile')
def profile():
    res = requests.get(OAuthConfig['profileURL'], headers={'Authorization': 'Bearer ' + oauth['a_token']})
    print(f"Status code: {res.status_code}")
    if res.status_code == 200:
        json = res.json()
        print(f"Received response: {json}")
        # Service account - no email
        oauth['email'] = json['preferred_username']
    else:
        oauth['email'] = '*** UNKNOWN ***'
    return render_template('index2.html', data=oauth)


@app.route('/logout')
def logout():
    params = {'redirect_uri': 'http://localhost:5000/'}
    query_str = urlencode(params)
    return redirect(OAuthConfig['logoutURL'] + query_str)


if __name__ == '__main__':
    app.run(debug=True)

Stop the Python Client AuthCode.py or OwnerPass.py if is already running, and start the Python Client ClientCredential.py to see this flow in action.

The following illustration shows the browser page after going through the items Access Token and User Profile:


Client Credentials Flow
Figure.32

This concludes our practical hands-on approach to understanding the OAuth2 and OpenID Connect standards.

!!! DISCLAIMER !!!

The code in this article is to understand OAuth2 and OIDC flows - it is PURELY for learning purposes.

References

The OAuth 2.0 Authorization Framework

OAuth 2.0 Security Best Current Practice

OAuth 2.0 and OpenID Connect (in plain English)

Keycloak - Open Source Identity and Access Management

GitHub - OAuth2 and OpenID Connect



© PolarSPARC