Search code examples
oauth-2.0luakeycloakopenid-connectbearer-token

Unable to authenticate with Keycloak using Bearer Token


I have a project that has a Python/Django backend. It sits behind Keycloak for authentication. Everything works, users can login using Keycloak.

We have a use case where a client needs to access some endpoints from the backend. We were hoping to provide the client with some sort of API Key so they can authenticate with Keycloak and reach the backend endpoints programmatically. After some research we thought of using an Offline Token. We are able to get the token and are trying to add the access token as the Bearer Token. The issue is we are unable to get past the login screen.

We are using Keycloak 17.0.0.

Here is the workflow in Python:

# Get the token
data = {
    "client_id": <id>,
    "client_secret": <secret>,
    "username": "admin",
    "password": "foo",
    "grant_type": "password",
    "scope": "openid offline_access"
}

response = requests.post('http://keycloak:8080/realms/<realm>/protocol/openid-connect/token',  headers={'Content-Type': 'application/x-www-form-urlencoded'}, data=data)

# What is returned in the response
{
 "access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkWVQ3eWJOdWM3ODN5OU53d3VabmdTaHNSMV83T0xtQ2E5NW5HZHh5bXZvIn0.eyJleHAiOjE2NjI3NDU5NjgsImlhdCI6MTY2Mjc0NTY2OCwianRpIjoiZjcyZDBhYWUtOGFlMy00NjA1LTgzMWYtYzdiN2YzN2E2MzY1IiwiaXNzIjoiaHR0cDovL2tleWNsb2FrOjgwODAvcmVhbG1zL2FyY2FuZS1maXJlIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjJmNzM5ZGQ1LTQxZmUtNDVmNy1hYzBjLTM2NTU4N2MyNTE3MyIsInR5cCI6IkJlYXJlciIsImF6cCI6Im5naW54Iiwic2Vzc2lvbl9zdGF0ZSI6IjVjMGIzNGM2LTc2YWItNDJlNy04ZjBjLTljODEzNzA4ZjcwZCIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1hcmNhbmUtZmlyZSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIG9mZmxpbmVfYWNjZXNzIGVtYWlsIiwic2lkIjoiNWMwYjM0YzYtNzZhYi00MmU3LThmMGMtOWM4MTM3MDhmNzBkIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiQ2hyaXMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImdpdmVuX25hbWUiOiJDaHJpcyJ9.J3oPEDWw_c-L6jMySSBF1uNV9mjl7d8AFwky8kN71qhTEJLDJFWKznIQ0sAMK8pGenbYOQFGDnAiL5E5BGh5g97jkv-PK1xSsvjKjIEAJuH15FGIeHB8RSth7ZYPcca-2kzRsqfs9ueeKbe1IAMlMcFdKgX3qJa3MPsLLYWVBI5QbxTf068sWxAoWNWCQzYfyTnsZKnnbGEHzMyHyTjKaFfCHj8Y2lTw1RXeEVdts2ck8OVg5B66NHxu4KHQqnS2t3EhhX-vsovctrZ-yyX_KXkv9uaZ8OUjbsPcAFr0Ta8vcK5ay-FyXfmcApwp6JptNBcL4M54OHcYAV3wmJS5Tg",
 "expires_in":300,
 "refresh_expires_in":0,
 "refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlMWI1ZjRmNC0xY2RiLTQwZmUtYWI0OS0yODFmNWExNzE1Y2YifQ.eyJpYXQiOjE2NjI3NDU2NjgsImp0aSI6IjRkMDUwMWQyLTEwYzktNDcwYS1iNmExLWNmZTE3MGUwMzc5YyIsImlzcyI6Imh0dHA6Ly9rZXljbG9hazo4MDgwL3JlYWxtcy9hcmNhbmUtZmlyZSIsImF1ZCI6Imh0dHA6Ly9rZXljbG9hazo4MDgwL3JlYWxtcy9hcmNhbmUtZmlyZSIsInN1YiI6IjJmNzM5ZGQ1LTQxZmUtNDVmNy1hYzBjLTM2NTU4N2MyNTE3MyIsInR5cCI6Ik9mZmxpbmUiLCJhenAiOiJuZ2lueCIsInNlc3Npb25fc3RhdGUiOiI1YzBiMzRjNi03NmFiLTQyZTctOGYwYy05YzgxMzcwOGY3MGQiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIG9mZmxpbmVfYWNjZXNzIGVtYWlsIiwic2lkIjoiNWMwYjM0YzYtNzZhYi00MmU3LThmMGMtOWM4MTM3MDhmNzBkIn0.DOu_zKq5WFI4PcTn5Qpe-VFwOj-aLtvc3q9SACs51Ew",
 "token_type":"Bearer",
 "id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkWVQ3eWJOdWM3ODN5OU53d3VabmdTaHNSMV83T0xtQ2E5NW5HZHh5bXZvIn0.eyJleHAiOjE2NjI3NDU5NjgsImlhdCI6MTY2Mjc0NTY2OCwiYXV0aF90aW1lIjowLCJqdGkiOiI0MDI5ZmI1ZC05ZDQwLTQ5NTQtYmNiMC02MmYzOTU2YjdmOTIiLCJpc3MiOiJodHRwOi8va2V5Y2xvYWs6ODA4MC9yZWFsbXMvYXJjYW5lLWZpcmUiLCJhdWQiOiJuZ2lueCIsInN1YiI6IjJmNzM5ZGQ1LTQxZmUtNDVmNy1hYzBjLTM2NTU4N2MyNTE3MyIsInR5cCI6IklEIiwiYXpwIjoibmdpbngiLCJzZXNzaW9uX3N0YXRlIjoiNWMwYjM0YzYtNzZhYi00MmU3LThmMGMtOWM4MTM3MDhmNzBkIiwiYXRfaGFzaCI6InNuVW11a3I1VUZWbTRyYVlxM1FxVlEiLCJhY3IiOiIxIiwic2lkIjoiNWMwYjM0YzYtNzZhYi00MmU3LThmMGMtOWM4MTM3MDhmNzBkIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiQ2hyaXMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImdpdmVuX25hbWUiOiJDaHJpcyJ9.AUGLQUiA8gyz_yEz5mShGdgVS9vQx_LbmJ6Xh9sEqTtIo9rDgflhKBlGK_b8V8KHir1NExtcuLAlSdMAwJWZ02IzMPNazkYEqxM_PMJh3nLXV6Q7Ph9a4BoPsN2xs6c9BxhlrXKgN1NChm38cKZHnBzw0ZlXcGTJfJdQvGjO8GGiXlZuzo9JioCByn-ZQvWtfepEHKZREx6rVcahSBM5PNG1i8GATRbAIxWpl88CRwv6r9OHXcvdjEEZo8Jl4yePumchyEo9NGMnf2Vk0Alp-cZv90AIO91uUAwo6a3P-iT-rJvk-tfJVed7XzxDqOaUA4ZMD9kCmZCyERolglV2QQ",
 "not-before-policy":0,
 "session_state":"5c0b34c6-76ab-42e7-8f0c-9c813708f70d",
 "scope":"openid profile offline_access email"
}

# Make request to the backend endpoint
response = requests.get('http://localhost/api/foo', headers={'Authorization': 'Bearer ' + response.json().get('access_token')})

What gets returned in the response is just the Keycloak login page:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" class="login-pf">

<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="robots" content="noindex, nofollow">

            <meta name="viewport" content="width=device-width,initial-scale=1"/>
    <title>Sign in to ***</title>
...

I'm not sure why we are not able to get past the login page. I thought if we provide the access token in the header we could get past the login page and access the backend endpoints.

Here are some of our relevant files:

dev.conf Nginx configuration

server {
  listen 80;
  server_name localhost;
  charset utf-8;
  client_max_body_size 10000m;
  resolver 127.0.0.11 valid=30s ipv6=off;
  resolver_timeout 10s;
  
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  # login with keycloak
  access_by_lua_file /etc/nginx/lua/auth.lua;
...

auth.lua Lua file used in nginx configuration

local opts = {
  redirect_uri_path = "/redirect_uri",
  accept_none_alg = true,
  discovery = "http://keycloak:8080/realms/<realm>/.well-known/openid-configuration",
  client_id = <id>,
  client_secret = <secret>,
  redirect_uri_scheme = "http",
  logout_path = "/logout",
  redirect_after_logout_uri = "http://keycloak:8080/realms/<realm>/protocol/openid-connect/logout",
  post_logout_redirect_uri = "http://localhost",
  redirect_after_logout_with_id_token_hint = false,
  revoke_tokens_on_logout = true,
  session_contents = {id_token=true,access_token=true,user=true}
}
-- call introspect for OAuth 2.0 Bearer Access Token validation
local res, err = require("resty.openidc").authenticate(opts)
if err then
  ngx.status = 403
  ngx.say(err)
  ngx.exit(ngx.HTTP_FORBIDDEN)
end

-- decode and send the user roles for this session
local jwt = require "resty.jwt"
local jwt_obj = jwt:load_jwt(res.access_token)
local cjson = require "cjson"
ngx.log(ngx.DEBUG, "res.access_token.sub=", cjson.encode(jwt_obj))

ngx.req.set_header("ACCESS-TOKEN", cjson.encode(jwt_obj))

Our Keycloak settings

We're unable to get past the login screen and access our backend endpoints. Is there something in our settings? I was wondering if its the logic in our Lua file. Also is there a better way for the client to access our endpoints?

Any help would be appreciated.


Solution

  • I was able to solve the issue. I had to change the code in the Lua file.

    Here is what I changed it to:

    local opts = {
      redirect_uri_path = "/redirect_uri",
      accept_none_alg = true,
      discovery = "http://keycloak:8080/realms/<realm>/.well-known/openid-configuration",
      client_id = <id>,
      client_secret = <secret>,
      redirect_uri_scheme = "http",
      logout_path = "/logout",
      redirect_after_logout_uri = "http://keycloak:8080/realms/<realm>/protocol/openid-connect/logout",
      post_logout_redirect_uri = "http://localhost",
      redirect_after_logout_with_id_token_hint = false,
      revoke_tokens_on_logout = true,
      session_contents = {id_token=true,access_token=true,user=true}
    }
    
    local oidc = require("resty.openidc")
    -- call bearer_jwt_verify for OAuth 2.0 JWT validation
    local res, err, access_token = oidc.bearer_jwt_verify(opts)
    if err or not res then
      -- call authenticate for OpenID Connect user authentication
      res, err = oidc.authenticate(opts)
      access_token = res.access_token
      if err then
        ngx.status = 403
        ngx.say(err)
        ngx.exit(ngx.HTTP_FORBIDDEN)
      end
    end
    -- decode and send the user roles for this session
    local jwt = require "resty.jwt"
    local jwt_obj = jwt:load_jwt(access_token)
    local cjson = require "cjson"
    ngx.log(ngx.DEBUG, "res.access_token.sub=", cjson.encode(jwt_obj))
    
    ngx.req.set_header("ACCESS-TOKEN", cjson.encode(jwt_obj))
    

    I check for the bearer token using bearer_jwt_verify. If there is none set I then revert to authenticating as before with authenticate.