OIDC Login 구현해보기 Part-3

2023. 1. 24. 22:33Development

앞서 Part에서 Frontend 측에서 Browser를 통해 IDP를 거쳐 code를 받는 것 까지 진행되었습니다. Confidential Type의 인증은 최종적으로 Secret을 이용하여 access token을 받게 되는 데 이 Secret은 Backend 에서 보관하고 있는 것이 안전할 것입니다. Static에 해당하는 부분은 Browser에 보여질 때 실제로 Client측에 그대로 Download 되어지기 때문에 Client측에서 Secret을 사용하려고 하면 그대로 노출이 되게 될 것 입니다.

앞서 User가 ID, PASSWORD를 입력해서 받은 CODE를 가지고 backend로 가져와서 실제 Token을 발급받고, 이 Token을 이용해서 IDP로 부터 USER의 정보도 받아보도록 하려고 합니다.

Backend로 Code를 전달

[Send code & redirect URL] Step부터 진행 하려합니다.

Backend Server로 IDP에게서 받은 code와 redirect URL 정보를 건네주는 과정입니다. 이 부분은 실제 OIDC Login 과정 중에서 가장 IDP와 연관이 없는 부분같이 느껴지지만 생략시 실제 code를 사용하는 위치가 혼란스러울 수 있을 것 같아서 건네주는 부분을 샘플 코드로 잠깐 소개하고 넘어가고자 합니다.

[Part-3 에서 Redirect된 후 Data를 backend로 넘기는 코드 추가]

import React, { useEffect } from "react";
import { requestPost} from "../../util/RequestData"

export default function Authorizing(props) {

  useEffect( () => {
    let params = new URLSearchParams(props.location.search);
    console.log(params.toString())

    let url = "http://localhost:4000?redirect_uri=http://172.22.130.112:3000/authorizing&code=e3d67040-4b03-4461-bbf3-ead1e261106d.2985896d-89e4-4475-99b0-de8dec99f40c.f9290787-76ef-4c5d-bf96-57b0e6568646"
    requestPost(url, {}, (response) => {
      console.log(response.data)
    })
  })

  return (
      <div>
        Redirected
      </div>
  ) 
}

POST API를 통해 backend server(여기선 localhost에 4000번 port로 listen하는 서버가 있다고 가정하겠습니다) 쪽으로, 넘겨받은 code와 인증시 사용한 Redirect URL을 건네줍니다.


[requestPost 함수]

import axios from "axios";

export function requestPost(url, data, response) {
  axios({
    method: "post",
    headers: { "Content-Type": "application/json" },
    url: url,
    data: JSON.stringify({
      ...data,
    })
  })
    .then(response)
    .catch(response);
}

Backend 동작

우선 실제 구현에 앞서 Code를 이용해서 IDP와의 동작을 테스트 해보도록 하겠습니다.
먼저 Keycloak에서 필요한 값을 재확인 해봅니다.

 

token 획득을 위한 endpoint와 secret

curl -d "grant_type=authorization_code&client_id=oidc-test&client_secret=647461d5-717b-4698-aeb3-52677d1baec0&redirect_uri=http://172.22.130.112:3000/authorizing&code=2fc7903b-ddfe-48fa-87f7-2097d1e69a79.40515afa-9889-4e12-8097-d2b1b84c1ed1.f9290787-76ef-4c5d-bf96-57b0e6568646" -H "Content-Type: application/x-www-form-urlencoded" -X POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token

우선 Curl 명령을 이용해서 IDP와 code 및 redirect_uri를 이용해서 Token을 받아보겠습니다.
client_secret은 Keycloak에서 받을 수 있는 client의 암호키 같은 격이며, 이 값을 안전하게 감추기 위해 backend에서 이 역할을 수행한다고도 할 수 있을 것 입니다. redirect_uri는 최초 code를 받을 때 사용했던 uri와 동일해야합니다. keycloak에 token을 요청한 결과는 아래와 같습니다.


{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2emhPM19wV182R1B3NThEeURxQkk5a0E5clJ5cVpkTC11MkZIb01RNmR3In0.eyJqdGkiOiI3MWRmYTBlNC1hYTU3LTQ3ZDYtYmQwMi04NDA0ZGJkMGEwMDAiLCJleHAiOjE1ODQ1NjA5OTcsIm5iZiI6MCwiaWF0IjoxNTg0NTYwOTM3LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiZWJjYmU2YWQtYjMyNy00ODhmLTlkZTQtMjM3Yjk4MTAwYzFjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoib2lkYy10ZXN0IiwiYXV0aF90aW1lIjoxNTg0NTYwMjEyLCJzZXNzaW9uX3N0YXRlIjoiNDA1MTVhZmEtOTg4OS00ZTEyLTgwOTctZDJiMWI4NGMxZWQxIiwiYWNyIjoiMCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUtcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJ2aWV3LXJlYWxtIiwibWFuYWdlLWlkZW50aXR5LXByb3ZpZGVycyIsImltcGVyc29uYXRpb24iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4ifQ.hIh0jWyVxiUoPvMYxDdiaveb5fmRkn8c5P0tVdcMwpznZheC2gxkej2DYqSQuaLtph5TXhDctwtz34ka_cizgIOB5CfMG2ynqAZ_LMIoseVDEGTCg8czRTLcN4fcOxIRcn3rRmK2U6WCYB_-FsV7sbZdw1cEG5Q4e72JqgRDtPOqqxgX8OUdf6Euwk7UjsvEDUgZrb2sGI8u1ykXPwudVqzSLFzzcqvt6ywDwof-nMwjfP7DyKfs5_ihyDHgaWRLOflzJlHBh1ToE8hMy0DP_iDyui3_LLH1ZEgdZfob4b9V0R9zcCthL_D6dDPfwIEO4xAsaLCtvRjtXPxh5XqAaw","expires_in":60,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1N2E3MWYzOC02NjYxLTRkMzQtOWE2YS1lMTRhZjRkNGU4MTYifQ.eyJqdGkiOiJlY2RhNGEzNi1lZGZlLTQwYTUtYTc0MS05NTgwZGY2YTk3ZjUiLCJleHAiOjE1ODQ1NjI3MzcsIm5iZiI6MCwiaWF0IjoxNTg0NTYwOTM3LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6ImViY2JlNmFkLWIzMjctNDg4Zi05ZGU0LTIzN2I5ODEwMGMxYyIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJvaWRjLXRlc3QiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI0MDUxNWFmYS05ODg5LTRlMTItODA5Ny1kMmIxYjg0YzFlZDEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiY3JlYXRlLXJlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctaWRlbnRpdHktcHJvdmlkZXJzIiwidmlldy1yZWFsbSIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUifQ.nnhB7krC9xFK__hPRV4PCsWCGVAvoYKA_0hhum0ydh8","token_type":"bearer","not-before-policy":1584531578,"session_state":"40515afa-9889-4e12-8097-d2b1b84c1ed1","scope":"email profile"}

access_token을 획득했습니다, 이제 token을 이용해 login한 유저의 정보를 얻어보도록 하겠습니다.


이 또한 먼저 curl을 이용해서 동작확인을 해보겠습니다.

$ curl -d "access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2emhPM19wV182R1B3NThEeURxQkk5a0E5clJ5cVpkTC11MkZIb01RNmR3In0.eyJqdGkiOiI2OTc2YmM0My02ZjBhLTRkYTMtOWUxOC03Mjc2NTg5MzdhMTkiLCJleHAiOjE1ODQ1NjIxNjYsIm5iZiI6MCwiaWF0IjoxNTg0NTYyMTA2LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiZWJjYmU2YWQtYjMyNy00ODhmLTlkZTQtMjM3Yjk4MTAwYzFjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoib2lkYy10ZXN0IiwiYXV0aF90aW1lIjoxNTg0NTYwMjEyLCJzZXNzaW9uX3N0YXRlIjoiNDA1MTVhZmEtOTg4OS00ZTEyLTgwOTctZDJiMWI4NGMxZWQxIiwiYWNyIjoiMCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUtcmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJ2aWV3LXJlYWxtIiwibWFuYWdlLWlkZW50aXR5LXByb3ZpZGVycyIsImltcGVyc29uYXRpb24iLCJjcmVhdGUtY2xpZW50IiwibWFuYWdlLXVzZXJzIiwicXVlcnktcmVhbG1zIiwidmlldy1hdXRob3JpemF0aW9uIiwicXVlcnktY2xpZW50cyIsInF1ZXJ5LXVzZXJzIiwibWFuYWdlLWV2ZW50cyIsIm1hbmFnZS1yZWFsbSIsInZpZXctZXZlbnRzIiwidmlldy11c2VycyIsInZpZXctY2xpZW50cyIsIm1hbmFnZS1hdXRob3JpemF0aW9uIiwibWFuYWdlLWNsaWVudHMiLCJxdWVyeS1ncm91cHMiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4ifQ.IVCu2ki44a41QVyqmMNTJsiGKfXVaCob5oO4TEdE_76-7djEgvySL5p7z7_fLfuKzoucHj-DI0LxL5XbLyuiQN-kTFWCMvKtVHKrbf6ZsREc6aFZwmUwtBJcpq3tEJiwfBHCWbPc8SRpoDZZt50nj8U3VrMXEiCYGlDKLBftL3o0woTBKuV2yQnr5CyELM1bcHSe1PqimqYZrzcZhywFcjJoKn65k04zcEjuc8HYs59ZDstXO7X-eFXrbW52-Fte6M_5jG7LNj4UVogfN82JicCEwGPMNx2NvDvrfVUlF-ML0QTPBV0XSPPv0J48vy2XjAiob77lHikWLytbjOaaLw" -X POST http://localhost:8080/auth/realms/master/protocol/openid-connect/userinfo

access_token을 userinfo URL에 전달해주면 아래와 같이 Userinfo가 넘어옴을 확인할 수 있습니다.

{"sub":"ebcbe6ad-b327-488f-9de4-237b98100c1c","email_verified":false,"preferred_username":"admin"}

Keycloak에서 어떤 정보를 내려줄지 추가 설정을 해주게 되면 IDP가 갖고 있는 여러 정보 Email, Name 등을 더 받을 수 있습니다.

Backend code로 구현

func codeVerification() map[string]string {

    data := url.Values{}
    data.Set("grant_type", "authorization_code")
    data.Set("client_id", "oidc-test")
    data.Set("client_secret", "647461d5-717b-4698-aeb3-52677d1baec0")
    data.Set("redirect_uri", "http://172.22.130.112:3000/authorizing")
    data.Set("code", "57f961bf-265f-49cc-a3d0-a3afe929d778.40515afa-9889-4e12-8097-d2b1b84c1ed1.f9290787-76ef-4c5d-bf96-57b0e6568646")

    req, _ := http.NewRequest("POST", serverInfo.TokenUrl, strings.NewReader(data.Encode())) 
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    respBody, err := ioutil.ReadAll(resp.Body)
    if err == nil {
      str := string(respBody)
      var ad map[string]string
      json.Unmarshal([]byte(str), &ad)
        return ad
    }    
    return nil

위 실행결과는 ad라는 map안에 return받은 token관련 정보들이 남게 됩니다.


func getUserInfo() map[string]interface{} {

    data := url.Values{}
    data.Set("access_token", "생략")

    req, _ := http.NewRequest("POST", serverInfo.UserInfoUrl, strings.NewReader(data.Encode())) 
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    respBody, err := ioutil.ReadAll(resp.Body)
    if err == nil {
        str := string(respBody)
        var ui map[string]interface{}
        json.Unmarshal([]byte(str), &ui)
        return ui
    }
    return nil
}

Token 값은 너무 길어 생략하였지만, curl로 진행했던 동작을 go코드로 옮긴 것 입니다.
초반에도 이야기했지만, Backend 또한 python, java등 언어에 상관없이 위에 동작을 수행하도록 로직을 작성하면 됩니다.


최종적으로 전달 받은 정보와 Token등은 REST API 마지막에 다시 Client(Browser) 측으로 return해주던 Cookie를 굽는 방법등으로 넘겨주고 그 다음부터 Client측의 Request에는 해당 Token을 포함시켜서 보내주면 이를 이용해서 권한을 확인하고 Resource 접근을 허용해주는 식으로 동작하면 될 것 입니다.

참고로 Token은 IDP에서 넘겨주는 것을 그대로 사용하는 방법도 있겠지만, IDP의 인증 후에는 자체 Token으로 대체해서 관리하는 것 또한 방법일 것 입니다.


이로서 OIDC Login 과정을 간단한 방법으로 구현해보았습니다.

Code가 한번의 실수나 시간만료로 인해 금방 유효하지 않게 되기 때문에
샘플 코드에서 코드의 값들이 자꾸 변하는 것이 보일탠데, 전체 흐름을 보고 따라가면 전체 컨택스트를 이해하는 데는 문제가 없을 것이라고 생각됩니다.
다음에 다 까먹고 다시볼 때 도움이 될 수 있도록 ㅠㅠ

'Development' 카테고리의 다른 글

GSON - Composite pattern class  (2) 2023.01.24
Design pattern - Composite pattern  (0) 2023.01.24
golang GO routine channel 테스트  (0) 2023.01.24
OIDC Login 구현해보기 Part-2  (0) 2023.01.24
OIDC Login 구현해보기 Part-1  (0) 2023.01.24