본문 바로가기

기타/Keycloak

keycloak js adapter 사용법 정리

keycloak js adapter 를 쓰면서 알게 된 내용을 정리해보자
(수정사항이나 피드백 환영합니다.)

ResponseMode 지정

  • keycloak instance 를 init 을 이용해 초기화 할 때, responseMode 을 fragement or query 로 지정해 줄 수 있다.

  • fragement 가 default 다.

  • fragment 로 해놓을 경우 뒤에 # 이 붙은 fragment 형태로 param 이 붙어서 온다.

    (fragment 로 붙어 있는 인자들은 http request 로 보내지지 않는다.)

1) fragment 로 할 경우

https://redirect\_uri/#params

2) query 로 해놓을 경우

https://redirect\_uri/?params

keycloak js adapter 의 typescript 용 keycloak.d.ts 에서 responseMode 값에 주석 내용을 보면 fragment 를 권장하고 있다.

/**
         * Set the OpenID Connect response mode to send to Keycloak upon login.
         * @default fragment After successful authentication Keycloak will redirect
         *                   to JavaScript application with OpenID Connect parameters
         *                   added in URL fragment. This is generally safer and
         *                   recommended over query.
         */

response mode 에 따른 redirect uri 파싱

redirect uri 에 담겨온 파라미터를 어떻게 파싱하는 걸까
keycloack.js 코드를 보면 url 을 파싱하는 함수가 있다.

function parseCallbackUrl(url) {

// 생략. 많은 코드들이 있어요


   var queryIndex = url.indexOf('?');
   var fragmentIndex = url.indexOf('#');

   var newUrl;
   var parsed;

   if (kc.responseMode === 'query' && queryIndex !== -1) {
      newUrl = url.substring(0, queryIndex);
      parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
      if (parsed.paramsString !== '') {
          newUrl += '?' + parsed.paramsString;
       }
       if (fragmentIndex !== -1) {
           newUrl += url.substring(fragmentIndex);
        }
    } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {
         newUrl = url.substring(0, fragmentIndex);
         parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
         if (parsed.paramsString !== '') {
              newUrl += '#' + parsed.paramsString;
          }
      }

     if (parsed && parsed.oauthParams) {
         if (kc.flow === 'standard' || kc.flow === 'hybrid') {
             if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {
                  parsed.oauthParams.newUrl = newUrl;
                  return parsed.oauthParams;
             }
        } else if (kc.flow === 'implicit') {
            if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {
                 parsed.oauthParams.newUrl = newUrl;
                 return parsed.oauthParams;
            }
        }
      }
    }

parseCallbackUrl 함수를 요약하자면

1) responseMode 가 fragment, query 인지에 따라 parameter 를 파싱 한다.
2) keycloack flow가 standard (authorization code 방식인 듯 하다.) or hyprid 인지 standard 인지에 따라
파싱한 파라미터를 확인한다.

3) authorization code 일 경우에는 code 를, implicit 일 경우에는 acess_token 을 받아왔는지 보고있다.

Authorization Code 와 Access Token 교환

  • 해당 작업은 keycloak flow 가 implicit 가 아닐때만 일어난다.

keycloack.js 에서 processCallback 함수가 이 작업을 해주는 듯 하다.
첫번째 인자로는 parseCallback 의 return 값이 들어간다.
parseCallback 은 redirect_uri 에서 들어온 정보들을 파싱해서 넘겨준다. 여기에 code 정보가 들어있다.

function processCallback(oauth, promise) {
            var code = oauth.code;
            var error = oauth.error;
            var prompt = oauth.prompt;

            var timeLocal = new Date().getTime();
            // 생략 ...

            // implicit 모드가 아니고 code 가 있을 경우에 access token 을 교환하기 위한 request 를 만든다.
            if ((kc.flow != 'implicit') && code) {
                var params = 'code=' + code + '&grant_type=authorization_code';
                var url = kc.endpoints.token();

                var req = new XMLHttpRequest();
                req.open('POST', url, true);
                req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');

                params += '&client_id=' + encodeURIComponent(kc.clientId);
                params += '&redirect_uri=' + oauth.redirectUri;

                if (oauth.pkceCodeVerifier) {
                    params += '&code_verifier=' + oauth.pkceCodeVerifier;
                }

                req.withCredentials = true; //withCredential 을 true 로 설정한다.

                req.onreadystatechange = function() { //응답이 왔을 때의 callback 함수 동작
                    if (req.readyState == 4) {
                        if (req.status == 200) {

                            var tokenResponse = JSON.parse(req.responseText);
                            authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard');
                            scheduleCheckIframe();
                        } else {
                            kc.onAuthError && kc.onAuthError();
                            promise && promise.setError();
                        }
                    }
                };

                req.send(params);
            }

Keycloak-js 의 acess token 교환 방식

보통 Authorization Code 방식이라 하면, authorization code 와 미리 발급받은 client sercet 을 가지고, 자신이 client 임을 인증하고,
access token 을 발급 받는다.

그러나 위 과정을 보면 client secret 을 acess token 교환시 같이 주지 않는다.
심지어 client type 도 public 이라 client secret 도 없는 상태다.

이를 자세히 보기 위해서는 client type 부터 알 필요가 있다.

Client Type

Oauth2.0 에서는 client type 을 confidential 과 public 두 가지로 정의한다.
https://oauth.net/2/client-types/
https://auth0.com/docs/applications/confidential-and-public-applications#grant-types

Confidential type

  • authorization server에 안전하게 인증할 수 있는 application 이다.
  • 예를 들어, 등록된 sercret 을 안전하게 유지할 수 있어야 한다.
  • secret 을 저장 할 수 있는 신뢰가능한 백엔드 서버가 필요하다.

Grant Type

다음과 같은 것들이 confidential application 으로 간주된다.

  • Authorization Code Flow 를 사용하는 안전한 backent 가 있는 web applications
  • Resource Owner Password Flow
  • Resource Owner Password Flow with realm support
  • A machine-to-machine (M2M) application that uses the Client Credentials Flow

Public type

등록된 client secret 을 사용할 수 없다. 브라우저나 mobile device 에서 실행되는 application 들이 있다.

Grant Type

client secret 을 요구하지 않는 grant type 만 사용 할 수 있다.
다음과 같은 것들이 public application 으로 간주된다.

  • Authorization Code Flow with PKCE 를 사용하는 native desktop 이나 mobile application
  • Implicit Flow grant 를 사용하는 client side 웹 application (singe-page app 같은)

Authorization Code + PKCE

keycloak-js 는 client 가 public type 이다. (secret 이 없다.)
그러나 authorization code 를 acess token 과 교환하기 때문에 impicit flow 는 아니다.

위 public client type 에서 말한 Authorization Code Flow with PKCE 가 keycloak-js 가 쓰는 방식으로 보인다.

정정한다. keycloak init 옵션에서 pkceMethod 를 지정할 수 있는데, 해당 method 를 지정하지 않으면 pkce 는 쓰이지 않는것으로 보인다.

실제 processCallback 코드를 보면 pkce 를 param 으로 넣어주는 부분이 있다.

if (oauth.pkceCodeVerifier) {
   params += '&code_verifier=' + oauth.pkceCodeVerifier;
}

PKCE (Proof Key for Code Exchange)

auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce

 

Auth0 Docs

Get started using Auth0. Implement authentication for any kind of application in minutes.

auth0.com

PKCE 는 public client 에서도 사용 할 수 있도록 standard code flow (authorization code) 에 추가 된 것이다.

Code Verifier와 Code Challenge 가 추가 된다.

  • code verifier : 48~128 글자수 가진 random string
  • code challenge : 선택한 hash 알고리즘으로 code_verifier 를 hasing 한 후 base 64 로 인코딩 한 값

다음과 같은 과정을 거친다고 한다.

  1. authorization server 에 client_id & redirect_uri 넘겨줄 때 code_challenge 와 hash 함수 종류를 넘겨준다.
  2. authorization server 에 code 전달 할때,code_verifier 를 같이 넘긴다.
  3. authorization server는 code_challenge 와 전달받은 code_verifier를 해싱 + base64 인코딩해서 비교한다.
  4. 검증이 완료 되면 access token 을 넘겨준다.

keycloak-js 에서 pkce 사용

  1. 먼저 client 에 pkce 사용을 지정해준다.
    keycloak admin console 에서 지정해줄 수 있다.

  1. init 함수에서 pkce method 를 지정해준다.
    1번만 설정한 상태로 client_id 전달시에, code challenge 가 오지 않았다는 에러가 뜬다.

    this.keycloak_
       .init({
        // 기타 개인 설정들
         pkceMethod: 'S256',
       })

실제 서버 요청시 url

  1. pkce mode off , fragment mode on 인 경우 client_id, redirect_uri 등 전해줄 때
    keycloak 로그인 페이지에 전달된 url 이다

    https://[ip]/openid-connect/auth?
    client_id=[clinet_id]&
    redirect_uri=[redirect_rui]&
    state=[정체모를 string]&
    response_mode=fragment&
    response_type=code&
    scope=openid&
    nonce=[정체모를 string]

'기타 > Keycloak' 카테고리의 다른 글

react-keycloak, js-adapter  (0) 2021.03.25