Skip to content

Single Sign On

Daniel Jacob edited this page Dec 4, 2024 · 103 revisions
  • Trick :just do a CTRL+click (on Windows and Linux) or CMD+click (on MacOS) on to open links in a new tab

Purpose

  • Implement Single Sign-On (SSO) authentication for a web application without modifying it.

Implementation

  • We use OpenID Connect (OIDC) authentication mechanism which is a thin layer that sits on top of OAuth 2.0, with Keycloak as the identity provider (IdP), and OpenResty as the relying party (RP)

  • The OpenID standard defines a situation whereby a cooperating site can act as an RP, allowing the user to log into multiple sites using one set of credentials. The user benefits from not having to share their login credentials with multiple sites, and the operators of the cooperating site avoid having to develop their own login mechanism.

  • In an Nginx and Keycloak architecture:

    • Keycloak is the Authentication Provider (Identity Provider).
    • Nginx, with the right configuration, can act as an OAuth2 Proxy to interact with Keycloak, validate tokens, and enforce access control rules on backend APIs or applications.

Installation


Configuration

  • keycloak: we need to create a realm(*) (here Maggot):

(*) A realm is a space where you manage objects, including users, applications, roles, and groups. A user belongs to and logs into a realm. One Keycloak deployment can define, store, and manage as many realms as there is space for in the database

realm


  • keycloak: Then at least one user(*) has to be created:

(*) Keycloak users are stored locally and not connected to an external identity source. Local Keycloak users can be used to create and manage service accounts that are not associated with individual end users.

user




Authenticate users who want to access the web application

  • We need to create first client(*), to authenticate users who want to access the web application, involving a redirect to the identity provider

(*) Clients are entities that can request Keycloak to authenticate a user. Most often, clients are applications and services that want to use Keycloak to secure themselves and provide a single sign-on solution. Clients can also be entities that just want to request identity information or an access token so that they can securely invoke other services on the network that are secured by Keycloak.

  • We have the following sequence diagram of the standard flow:

flow1


  • keycloak: Creating and configuring a client : only the standard flow needs to be specified to make redirections

client


  • keycloak: We then create some roles(*) directly attached to the client

(*) These roles are specific to individual client applications within the realm. Each client can have its own set of roles that define access permissions specific to that application.

client_roles


  • keycloak: We then assign one of the roles to the previously created user.

user_roles


  • nginx: The corresponding configuration part in the nginx.conf file
require("sessions_store")
local opts = {
     redirect_uri_path = "/maggot/Maggot/redirect_uri",
     discovery = "http://10.0.0.106:8080/realms/Maggot/.well-known/openid-configuration",
     client_id = "maggot",
     client_secret = "GUWHrrBXnJp3dtT3Nl15olqDgyxaGGx2",
     scope = "openid email",
     access_token_expires_leeway = 30,
     renew_access_token_on_expiry = true,
     redirect_uri_scheme = "http",
     logout_path = "/maggot/logout",
     revoke_tokens_on_logout = true,
     redirect_after_logout_uri = "http://10.0.0.106:8080/realms/Maggot/protocol/openid-connect/logout?client_id=maggot",
     redirect_after_logout_with_id_token_hint = false,
     session_contents = {id_token=true, access_token=true},
     session_store = shared_dict_session_store("session_cache_realm1")
}

local res, err = require("resty.openidc").authenticate(opts,nil,nil,{name=opts.client_id, audience=opts.client_id})
if err then
     ngx.status = 403
     ngx.exit(ngx.HTTP_FORBIDDEN)
end

ngx.req.set_header("Authorization", "Bearer " .. res.access_token)

  • User's web browser: The sequence diagram of the standard flow can be traced with the development module of your browser:

net1 net2

  • Note: It is important to highlight that a session is managed per realm (here session_cache_realm1 for Maggot realm). This allows the realms to be isolated from each other. We can thus 1) either create another client dedicated to an application in the same realm, and in this case the connection is valid for all applications in the realm (role of SSO), 2) or create another realm with its own session. Connections for one realm do not interfere with another realm, as long as each realm has its own session.


  • User's web browser: When you want to access the application's web page (here http://10.0.0.106/maggot/), you are redirected to the keycloak login page (assuming you have chosen the maggot theme in the realm configuration regarding the login function (see How to customize the login page)

maggot-login


  • After authentication, we can retrieve the token within the Maggot application (or another one) to decode it to retrieve its payload.
  • See information related to certain programming languages : PHP, Python, R, Javascript or to certain frameworks: PHP Symfony, Django.


maggot-logout




Access to resources provided by the web API using a web token

  • We need to create a second client to directly access the resources provided by the web API using a JSON Web Token (JWT). We have the following sequence diagram of the direct access flow:

flow2


  • keycloak: Creating and configuring a client : the both 'direct access grants' and 'service accounts roles' are needed.

api-client


  • Note: In the same way as for the previous client, we create roles directly attached to the client and then assign one of the roles to the user.

  • nginx: The corresponding configuration part in the nginx.conf file
       local opts = {
           discovery = "http://10.0.0.106:8080/realms/Maggot/.well-known/openid-configuration",
           client_id = "api-maggot",
           client_secret = "FYFBOxpWl6spQ9of62ljGhR7v6NcnBS7",
           session_contents = {id_token=true}
       }

       -- call bearer_jwt_verify for OAuth 2.0 JWT validation
       local res, err = require("resty.openidc").bearer_jwt_verify(opts)
       if err then
           ngx.status = 403
           ngx.exit(ngx.HTTP_FORBIDDEN)
        end

  • CLI : Example of a bash script using cURL and ./jq
# Keycloak variables
CLIENT_ID='api-maggot'
CLIENT_SECRET='FYFBOxpWl6spQ9of62ljGhR7v6NcnBS7'
TOKEN_URL='http://10.0.0.106:8080/realms/Maggot/protocol/openid-connect/token'

# User credentials
USERNAME='djacob'
PASSWORD='djpassword'

# Web API URL
API_URL='http://10.0.0.106/maggot/metadata/frim1?format=maggot'

# Get Access Token
JSON=$(curl -s -H 'Content-Type: application/x-www-form-urlencoded' \
      -d "client_id=$CLIENT_ID" -d "client_secret=$CLIENT_SECRET" \
      -d 'grant_type=password' -d "username=$USERNAME" -d "password=$PASSWORD" $TOKEN_URL)

TOKEN=$(echo $JSON | jq -r '.access_token')
REFRESH_TOKEN=$(echo $JSON | jq -r '.refresh_token')

# Extracting the payload from the access token (optional)
echo $TOKEN | sed -e "s/\./\n/g" | head -n -1 | tail -1 | base64 --decode 2>/dev/null | jq

# Make a request to the protected API
# Note: API-KEY is needed in the header
curl -s -H 'accept: application/json' -H "API-KEY: XX" -H "Authorization: Bearer $TOKEN" \
     -X GET $API_URL; echo; echo;

  • Note: it is necessary to specify in the header of the request the 'API-KEY' variable with any value. Indeed it is by this means that we can differentiate between the two types of access (direct access vs redirection to the login page). See nginx.conf:
if ngx.req.get_headers()["API-KEY"] ~= nil then
... direct access ...
else
... redirection to the login page ...
end

  • Example of a payload of a JWT:
{
  "exp": 1729776742,
  "iat": 1729776442,
  "jti": "0d08be5c-e0b0-4b89-8680-848589d0eb9c",
  "iss": "http://10.0.0.106:8080/realms/Maggot",
  "aud": "account",
  "sub": "233a2dc3-b7df-46b2-9d5e-52bd17fd00b6",
  "typ": "Bearer",
  "azp": "api-maggot",
  "sid": "78b62332-463f-4271-a40f-b38e91546260",
  "acr": "1",
  "allowed-origins": [
    "*"
  ],
  "realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization",
      "default-roles-maggot"
    ]
  },
  "resource_access": {
    "api-maggot": {
      "roles": [
        "team1"
      ]
    },
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    },
    "maggot": {
      "roles": [
        "team1"
      ]
    }
  },
  "scope": "profile email",
  "email_verified": false,
  "name": "Daniel Jacob",
  "preferred_username": "djacob",
  "given_name": "Daniel",
  "family_name": "Jacob",
  "email": "[email protected]"
}

TODO

INRAE