Блокировка ключа: Пользователь может получить доступ к области, войдя в другую

#keycloak #openresty

Вопрос:

У меня есть клиент nginx/openresty для сервера блокировки ключей для авторизации с использованием openid. Я использую lua-resty-openidc, чтобы разрешить доступ к службам, расположенным за прокси-сервером.

Я создал двух клиентов в двух разных сферах для разных услуг.

Проблема в том , что после того, как пользователь проходит аутентификацию в первой области, например https://<my-server>/auth/realms/<realm1>/protocol/openid-connect/auth?response_type=codeamp;client_id=openrestyamp;state=........... , он также может напрямую получить доступ к другой службе realm2 .

Что здесь происходит? Как я могу гарантировать, что пользователь сможет получить доступ к клиенту только в той области, в которой он прошел аутентификацию?

Как я могу гарантировать, что после выхода из системы пользователь больше не сможет получить доступ, пока он не войдет в систему заново?

[Правка-подробности] Мой nginx.conf для двух сервисов приведен ниже. Пользователь сначала получает доступ https://<my-server>/service_1/ и перенаправляется на keycloack, чтобы ввести свой пароль для realm1. Он предоставляет его и может получить доступ к service_1.

Однако, если после этого он попытается получить доступ https://<my-server>/service_2/ , ему больше не нужно проходить аутентификацию, но он может войти в систему, хотя service_2 относится к клиенту в другой области, с другим секретом client_secret!

….. местоположение /service_1/ {

     access_by_lua_block {
        local opts = {
            redirect_uri_path = "/service_1/auth", -- we are send here after auth
            discovery = "https://<my-server>/keycloak/auth/realms/realm1/.well-known/openid-configuration",
            client_id = "openresty",
            client_secret = "<client1-secret>",
            session_contents = {id_token=true} -- this is essential for safari!
        }
        -- 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
    }

    # I disabled caching so the browser won't cache the site.
    expires           0;
    add_header        Cache-Control private;

    proxy_pass http://<server-for-service1>:port1/foo/;
    proxy_set_header Host $http_host;

    proxy_http_version 1.1;
    proxy_redirect off;
    proxy_buffering off;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

}
 

……………………..

 location /service_2/ {

    access_by_lua_block {
        local opts = {
            redirect_uri_path = "/service_2/auth", -- we are send here after auth
            discovery = "https://<my-server>/keycloak/auth/realms/realm2/.well-known/openid-configuration",
            client_id = "openresty",
            client_secret = "client2-secret",
            session_contents = {id_token=true} -- this is essential for safari!
        }
        -- 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
    }

    # I disabled caching so the browser won't cache the site.
    expires           0;
    add_header        Cache-Control private;

    proxy_pass http://<server-for-service2>:port2/bar/;
    proxy_set_header Host $http_host;

    proxy_http_version 1.1;
    proxy_redirect off;
    proxy_buffering off;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

}
 

[Правка-подробности 2]

Я использую lua resty openidc версии 1.7.2, но все, что я пишу, должно соответствовать версии 1.7.4, основанной на различии кода двух версий.

Из журналов уровня отладки я ясно вижу, что сеанс создается во время первого доступа, а затем повторно используется во второй области, что неверно, так как у второго доступа все еще есть токен для первой области… Вот как выглядит авторизация для realm2…:

 2021/04/28 12:56:41 [debug] 2615#2615: *4617979 [lua] openidc.lua:1414: authenticate(): session.present=true, session.data.id_token=true, session.data.authenticated=true, opts.force_reauthorize=nil, opts.renew_access_token_on_expiry=nil, try_to_renew=true, token_expired=false
2021/04/28 12:56:41 [debug] 2615#2615: *4617979 [lua] openidc.lua:1470: authenticate(): id_token={"azp":"realm1","typ":"ID","iat":1619614598,"iss":"https://<myserver>/keycloak/auth/realms/realm1","aud":"realm1","nonce":"8c8ca2c4df2...b26"
,"jti":"1c028c65-...0994f","session_state":"0e1241e3-66fd-4ca1-a0dd-c0d1a6a5c708","email_verified":false,"sub":"25303e44-...e2c1757ae857","acr":"1","preferred_username":"logoutuser","auth_time":1619614598,"exp":1619614898,"at_hash":"5BNT...j414r72LU6g"}
 

Комментарии:

1. «он также может напрямую получить доступ к другой услуге в режиме реального времени». что вы конкретно имеете в виду под этим?

2. добавлено объяснение и соответствующие детали моего nginx.conf….

3. Вы используете пользователя главной области для тестирования ? если это так, то это может произойти.

4. Нет, оба царства не являются царством хозяев… Кроме того, попытался изменить имена клиентов, чтобы они были разными в каждой области… Не повезло….

Ответ №1:

Ладно, это заняло у меня некоторое время. Также может быть так, что большинство учебных пособий оставляют эту уязвимость (только в настройках, где один nginx использует несколько областей), когда доступ к аутентификации в одной области позволит получить доступ к аутентификации в любой другой.

типичный вызов аутентификации из учебников там:

 location /service1/ {

    access_by_lua_block {
        local opts = {
            redirect_uri_path = "/realm1/authenticated",
            discovery = "https://<myserver>/keycloak/auth/realms/realm1/.well-known/openid-configuration",
            client_id = "client1",
            client_secret = <........>,
            session_contents = {id_token=true} -- this is essential for safari!
        }
        -- 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
    }

    # I disbled caching so the browser won't cache the site.
    expires           0;
    add_header        Cache-Control private;

    proxy_pass http://realm1-server:port/service1/;
    proxy_set_header Host $http_host;

    proxy_http_version 1.1;
    proxy_redirect off;
    proxy_buffering off;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

}
location /service2/ {
 <same for ream2> 
}
 

Похоже, на самом деле есть две проблемы

  1. Мы не проверяем идентификатор области (это уязвимость)
  2. Сеансы для двух областей кэшируются взаимозаменяемо (это приведет к ситуации, когда, если мы исправим (1), нам теперь будет разрешен доступ только к одной области и нам придется выйти из realm1 для доступа к realm2)

Решения: 1)Нам нужно явно проверить правильность области 2)Мы должны использовать одну таблицу сеансов для каждой области (обратите внимание, что, хотя это, по-видимому, также решает (1), это не так, если злоумышленник смешивает и сопоставляет идентификаторы сеансов со своим «специальным» браузером-по крайней мере, я думаю)

Для no2 не было документации, мне пришлось прочитать код openidc.lua, а оттуда код библиотеки, используемой этой библиотекой (session.lua)

Изменения заключаются в следующем: местоположение /сервис1/ {

     access_by_lua_block {
        local opts = {
            redirect_uri_path = "/realm1/authenticated",
            discovery = "https://<myserver>/keycloak/auth/realms/realm1/.well-known/openid-configuration",
            client_id = "client1",
            client_secret = <........>,
            session_contents = {id_token=true} -- this is essential for safari!
        }
        -- call introspect for OAuth 2.0 Bearer Access Token validation
        local res, err = require("resty.openidc").authenticate(opts,nil,nil,{name=opts.client_id})

        if (err or ( res.id_token.azp ~= opts.client_id ) ) then
            ngx.status = 403
            ngx.say(err)
            ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    }
    <..................no changes here................>
}
location /service2/ {
 <same for ream2> 
}