🌱 Infra/KeyCloak

[keycloak 맛보기 #5] Keycloak의 애플리케이션 통합

mini_world 2024. 12. 17. 22:29
목차 접기

참고자료

 


통합 방식 설명

 

Keycloak과 애플리케이션을 통합할 때,  Embedded와 Proxied 방식이 있다.
각 방법은 애플리케이션의 구조와 보안 요구 사항에 따라 선택할 수 있다.

구분 Embedded Proxied
설명 애플리케이션 코드 내에서 Keycloak 어댑터를 직접 사용하여 인증 및 권한 부여를 처리한다. 역방향 프록시를 사용하여  Keycloak과의 통신을 처리하는 역방향 프록시를 설정하여 애플리케이션 앞단에서 인증을 처리한다.
애플리케이션 코드와 독립적으로 Keycloak과의 통합을 관리한다.
장점 애플리케이션 코드에서 인증 흐름을 직접 제어할 수 있다.
다양한 인증 및 권한 부여 시나리오를 구현할 수 있다.
애플리케이션 코드에서 인증 로직을 제거하여 코드가 단순해진다.
인증 및 권한 부여 로직이 프록시에서 처리되므로 보안이 강화된다.
단점 복잡성 증가: 애플리케이션 코드에 인증 로직이 포함되어 복잡성이 증가할 수 있다.
보안 위험: 클라이언트 측에서 직접 토큰을 관리할 경우 보안 위험이 증가할 수 있다.
프록시 설정이 복잡할 수 있으며, 추가적인 인프라가 필요할 수 있다.
프록시에서 제공하는 기능에 의존하게 되어, 특정 인증 시나리오를 구현하는 데 제한이 있을 수 있다.

두 방식 모두 상황에 따라 선택할 수 있고, 때로는 함께 사용할 수 있다.

Certified OpenID Connect Implementations 에서 인증된 라이브러리를 사용하는것이 좋다.

 


Javascript 애플리케이션 통합

 

실습 진행

실습을 위해 Keycloak을 준비한다.

docker run -p 8080:8080 \
          -e KEYCLOAK_ADMIN=admin \
          -e KEYCLOAK_ADMIN_PASSWORD=admin \
          quay.io/keycloak/keycloak \
          start-dev

http://localhost:8080/으로 접속하여 새로운 realm을 생성했다. (옵션/생략가능)

 

새로운 client를 생성한다.

  • client id: javascript
  • root URL: http://localhost:8000
  • valid redirect URIs: http://localhost:8000/*

새로운 사용자도 생성한다.

 

이제 테스트 코드를 준비한다.

# 테스트 코드 다운로드
git clone https://github.com/PacktPublishing/Keycloak---Identity-and-Access-Management-for-Modern-Applications-2nd-Edition.git
cd ch7/keycloak-js-adapter

realm과 client를 생성했으니, 코드도 수정해주자.

# ch7/keycloak-js-adapter/public/keycloak.json
{
  "realm": "myservice",
  "auth-server-url": "http://localhost:8080",
  "ssl-required": "external",
  "resource": "javascript",
  "public-client": true
}
# ch7/keycloak-js-adapter/app.js
import express from 'express';
import stringReplace from 'string-replace-middleware';

const app = express();
const port = 8000;

app.use(stringReplace({
  KC_URL: process.env.KC_URL || "http://localhost:8080"
}));

app.use('/', express.static('public'));

app.listen(port, () => {
  console.log(`Listening on port ${port}.`);
});

keycloak은 로컬의 8080포트에서 운영되며, 어플리케이션은 8000포트로 올라오게 된다.
이제 실행해보자.

# 경로 잘 들어와있는지 한번 더 확인
cd Keycloak---Identity-and-Access-Management-for-Modern-Applications-2nd-Edition/ch7/keycloak-js-adapter
# 시작
npm install
npm start

브라우저에서 http://localhost:8000 접속해보자.

접속하면 바로 keycloak으로 연결되며, 로그인 하면 페이지에 접근할 수 있게 된다.

설명

코드를 확인해보자.

javascript를 8000번 포트로 띄우기 위해 실행하는 이 app.js코드에서는
string-replace-middleware를 사용하여 애플리케이션의 정적 파일을 제공할 때, 파일 내의 KC_URL 변수에 Keycloak 주소를 담는다.

// ch7/keycloak-js-adapter/app.js
import express from 'express';
import stringReplace from 'string-replace-middleware';

const app = express();
const port = 8000;

// 환경 변수 KC_URL을 사용하여 Keycloak 서버의 URL을 설정한다.
app.use(stringReplace({
  KC_URL: process.env.KC_URL || "http://localhost:8080"
}));

app.use('/', express.static('public'));

app.listen(port, () => {
  console.log(`Listening on port ${port}.`);
});

 

keycloak.json Keycloak 클라이언트 설정을 포함하고 있으며, Keycloak 라이브러리가 초기화될 때 해당 설정을 사용한다.
(Keycloak JavaScript 어댑터 라이브러리가 자동으로 로드함)

작동 방식은 아래와 같다.

  1. Keycloak 초기화: index.html에서 Keycloak() 객체를 생성하고 init 메서드를 호출할 때, 라이브러리는 기본적으로 keycloak.json 파일을 찾는다.
  2. 설정 로드: keycloak.json 파일이 public 디렉토리에 위치해 있으면, Keycloak 라이브러리가 이 파일을 자동으로 로드하여 필요한 설정을 적용한다.
# ch7/keycloak-js-adapter/public/keycloak.json
{
  "realm": "myservice",
  "auth-server-url": "http://localhost:8080",
  "ssl-required": "external",
  "resource": "javascript",
  "public-client": true
}

 

index.html은 Keycloak을 사용하여 사용자 인증을 처리하는 웹 페이지다. 코드는 길어서 설명만 추가했다.

<script src="KC_URL/js/keycloak.js"></script>

이 부분에서 KC_URL은 app.js에서 설정된 Keycloak 서버의 URL로 대체된다.
이 과정은 Keycloak 초기화의 일부로, Keycloak() 객체를 생성하고 init 메서드를 호출하여 Keycloak을 초기화한다.
이때 keycloak.json 파일이 자동으로 로드되며, Keycloak JavaScript 어댑터 라이브러리가 로드되어 클라이언트 측에서 Keycloak과의 통신을 처리할 수 있게 된다.

<!-- 생략 -->
      document.getElementById("logout").addEventListener("click", () => {
        keycloak.logout();
      });

      document.getElementById("showIdToken").addEventListener("click", () => {
        output(keycloak.idTokenParsed);
      });

      document
        .getElementById("showAccessToken")
        .addEventListener("click", () => {
          output(keycloak.tokenParsed);
        });

      document
        .getElementById("refreshToken")
        .addEventListener("click", async () => {
          await keycloak.updateToken(-1);
          output(keycloak.idTokenParsed);
          showProfile();
        });

      document
        .getElementById("showMyAccount")
        .addEventListener("click", async () => {
          await keycloak.accountManagement()
        });
<!-- 생략 -->

또한 코드내 버튼을 통해 클릭 이벤트 리스너를 추가하여 Keycloak의 기능을 수행할 수 있도록 했다.

  • 로그아웃: keycloak.logout()을 호출하여 사용자를 로그아웃한다.
  • 토큰 표시: keycloak.idTokenParsed)을 통해 토큰과 액세스 토큰을 JSON 형식으로 출력한다.
  • 토큰 갱신: keycloak.updateToken()을 호출하여 토큰을 갱신한다.
  • 계정 관리: keycloak.accountManagement()를 호출하여 사용자의 계정 관리 페이지로 이동한다.

 


Nodejs 애플리케이션 통합 (frontend)

 

실습 진행

실습을 진행해보자. 
위 단계에서 keycloak을 도커로 띄우고 realm을 생성했으니 해당 단계는 건너뛰고 바로 client부터 생성한다.

  • client id: nodejs
  • root URL: http://localhost:8000
  • client authentication: on

보안 client이므로, Credentials 탭에서 Secret을 복사해둔다.

 

이제 코드를 준비한다.

# 테스트 코드 다운로드
git clone https://github.com/PacktPublishing/Keycloak---Identity-and-Access-Management-for-Modern-Applications-2nd-Edition.git
cd ch7/nodejs/frontend

keycloak 설정에 맞게 코드를 수정한다.
위에서 복제한 client secret으로 값을 변경하고, 8000포트에서 애플리케이션이 시작될 수 있도록 코드를 수정한다.

// ch7/nodejs/frontend/app.js
var express = require('express');
var session = require('express-session');
var Keycloak = require('keycloak-connect');
var cors = require('cors');

var app = express();

app.use(cors());

var memoryStore = new session.MemoryStore();

app.use(session({
    secret: 'io9bBSukiwEyUekE62g88aeI4CKYUBTi', // 위에서 복사한 client secret 입력
    resave: false,
    saveUninitialized: true,
    store: memoryStore
}));

var keycloak = new Keycloak({ store: memoryStore });

app.use(keycloak.middleware());

app.get('/', keycloak.protect(), function (req, res) {
    res.setHeader('content-type', 'text/plain');
    res.send('Welcome!');
});

app.listen(8000, function () {         // 애플리케이션 포트 8000으로 수정
    console.log('Started at port 8000'); 
});
# ch7/nodejs/frontend/keycloak.json
{
  "realm": "myservice", 
  "auth-server-url": "${env.KC_URL:http://localhost:8080}",
  "resource": "nodejs",
  "credentials" : {
    "secret" : "io9bBSukiwEyUekE62g88aeI4CKYUBTi"
  }
}

이제 실행해보자

# 경로 잘 들어와있는지 한번 더 확인
cd Keycloak---Identity-and-Access-Management-for-Modern-Applications-2nd-Edition/ch7/nodejs/frontend
# 시작
npm install
npm start

로컬 브라우저에서 http://localhost:8000을 접속하면 아래와 같은 결과가 나온다.

설명

먼저 keycloak.json 은 Keycloak 클라이언트 설정을 포함한다.

위 javascript 실습에서도 keycloak.json파일이 사용되었는데 이는 javascript환경에서 주로 사용되는 표준으로, 
다른 언어에서는 XML등의 형식으로 사용할 수 도있다. (어뎁터에 따라 다름)

app.js 파일 내 모듈이 초기화될 때(var keycloak = new Keycloak({ store: memoryStore });)
해당 설정을 사용하여 Keycloak 서버와의 통신을 설정한다.

# ch7/nodejs/frontend/keycloak.json
{
  "realm": "myservice",
  "auth-server-url": "${env.KC_URL:http://localhost:8080}",
  "resource": "nodejs",
  "credentials" : {
    "secret" : "io9bBSukiwEyUekE62g88aeI4CKYUBTi"
  }
}

 

app.js에서는 keycloak모듈을 불러오고 초기화 한다. 

keycloak.json 그리고 app.js 두 곳에서 client secret이 모두 사용되었는데, 목적이 다르다.

  • [app.js] Express 세션의 secret:
    • Express의 세션 미들웨어는 사용자 세션을 관리하기 위해 사용된다.
    • app.js에 사용된 secret은 세션 ID 쿠키를 서명하는 데 사용되어, 클라이언트와 서버 간의 세션 데이터가 변조되지 않도록 보호한다.
  • [keycloak.json] Keycloak의 secret:
    • Keycloak의 secret은 클라이언트 인증을 위해 사용된다. 
    • Keycloak 서버와 애플리케이션 간의 안전한 통신을 보장하기 위해 사용되며, keycloak.json 파일에 저장된다.
    • Keycloak이 클라이언트를 식별하고 인증하는 데 필요하다.

var memoryStore = new session.MemoryStore(); 부분은 세션데이터를 로컬 메모리에 저장하겠다는 설정이며,
메모리 스토어는 일반적으로 개발환경에서만 사용된다. (서버가 재시작되면 세션데이터 모두 지워짐) 
따라서 보통은 별도의 데이터베이스(Mongodb, rdb, redis) 등을 활용하여 세션을 관리한다.

var express = require('express');
var session = require('express-session');
// Keycloak 모듈 불러오기
// 이 모듈은 Node.js 애플리케이션에서 Keycloak과의 통합을 지원함
var Keycloak = require('keycloak-connect');
var cors = require('cors');

var app = express();

app.use(cors());

// 메모리스토어 설정
// 세션 데이터를 저장하기 위해 메모리 스토어를 생성
var memoryStore = new session.MemoryStore();

app.use(session({
    secret: 'io9bBSukiwEyUekE62g88aeI4CKYUBTi',
    resave: false,
    saveUninitialized: true,
    store: memoryStore
}));

// Keycloak 인스턴스 생성
// Keycloak 인스턴스를 생성하고, 세션 스토어를 전달한다. 
// 이 인스턴스는 Keycloak과의 상호작용을 관리한다.
var keycloak = new Keycloak({ store: memoryStore });

// Keycloak 미들웨어 사용
// Keycloak 미들웨어를 Express 애플리케이션에 추가한다.
// 이 미들웨어는 요청을 가로채고, 인증 및 권한 부여를 처리한다.
app.use(keycloak.middleware());

// Keycloak으로 보호할 경로 설정
// keycloak.protect()를 사용하여 특정 경로를 보호한다. 
// 이 경로에 접근하려면 사용자가 인증되어야 합니다.
app.get('/', keycloak.protect(), function (req, res) {
    res.setHeader('content-type', 'text/plain');
    res.send('Welcome!');
});

app.listen(8000, function () {
    console.log('Started at port 8000');
});

 

 


Nodejs 애플리케이션 통합 (backend)

 

실습 진행

실습을 진행해보자. 위에서 생성한 client를 재활용한다.

  • client id: nodejs
  • root URL: http://localhost:8000
  • client authentication: on

코드를 준비한다.

# 테스트 코드 다운로드
git clone https://github.com/PacktPublishing/Keycloak---Identity-and-Access-Management-for-Modern-Applications-2nd-Edition.git
cd ch7/nodejs/backend

keycloak 설정에 맞게 코드를 수정한다.
위에서 복제한 client secret으로 값을 변경하고, 8001포트에서 애플리케이션이 시작될 수 있도록 코드를 수정한다.

// ch7/nodejs/backend/app.js
var express = require('express');
var Keycloak = require('keycloak-connect');

var app = express();

var keycloak = new Keycloak({});

app.use(keycloak.middleware());

app.get('/hello', keycloak.protect(), function (req, res) {
  res.setHeader('content-type', 'text/plain');
  res.send('Access granted to protected resource');
});

app.listen(8001, function () {
  console.log('Started at port 8001');
});
# ch7/nodejs/backend/keycloak.json
{
  "realm": "myservice",
  "bearer-only": true,
  "auth-server-url": "${env.KC_URL:http://localhost:8080}",
  "resource": "nodejs"
}

이제 실행해보자.

# 경로 잘 들어와있는지 한번 더 확인
cd Keycloak---Identity-and-Access-Management-for-Modern-Applications-2nd-Edition/ch7/nodejs/backend
# 시작
npm install
npm start

로컬 브라우저에서 http://localhost:8001에 접속하면 아래처럼 Cannot GET / 오류가 발생한다.

그 이유는 AccessToken이 없기때문이다. 
CLI로 AccessToken을 가져와보자.

# curl -X POST http://localhost:8080/realms/myservice/protocol/openid-connect/token \
#	 -u "nodejs:io9bBSukiwEyUekE62g88aeI4CKYUBTi" \
#	 -H "Content-Type: application/x-www-form-urlencoded" \
#	 -d "username=test01" -d "password=1234"  \
#	 -d "grant_type=password" | jq -r '.access_token'

curl -X POST http://localhost:8080/realms/${realm}/protocol/openid-connect/token \
    -u "${client_id}:${client_secret}" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "username=${user_name}" -d "password=${user_password}" \
    -d "grant_type=password" | jq -r '.access_token'

결과로 Access Token을 얻었다. (참고: 브라우저를 통하지 않고 사용자ID/PW를 이용하여 접근토큰을 받으려면  Direct access grants이 허용되어있어야 하며, keycloak은 기본값이 허용이다.)

이제 이 Access Token을 가지고 Backend 서버에 접근해보자.

# 예시, 각 변수 올바르게 설정 필요
# export access_token=$(curl -X POST http://localhost:8080/realms/myservice/protocol/openid-connect/token \
# -u "nodejs:io9bBSukiwEyUekE62g88aeI4CKYUBTi" \
# -H "Content-Type: application/x-www-form-urlencoded" \
# -d "username=test01" -d "password=1234"  \
# -d "grant_type=password" | jq -r '.access_token')

export access_token=$(curl -X POST http://localhost:8080/realms/${realm}/protocol/openid-connect/token \
-u "${client_id}:${client_secret}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=${user_name}" -d "password=${user_password}" \
-d "grant_type=password" | jq -r '.access_token')

echo $access_token

curl -v GET http://localhost:8001/hello \
-H "Authorization: Bearer "$access_token

그럼 아래와 같은 결과가 나온다.

만약 AccessToken 없이 요청한다면 실패하게 된다.

 

설명

여기에서 실습한 nodejs/backend 는 Keycloak을 사용해서 사용자를 인증하는것이 아닌,
AccessToken을 활용하여 인증된 요청을 처리하는 리소스 서버로서의 역할을 한다.

// ch7/nodejs/backend/app.js
var express = require('express');
var Keycloak = require('keycloak-connect');

var app = express();

var keycloak = new Keycloak({});

// keycloak 미들웨어 사용
app.use(keycloak.middleware());

// 보호된 리소스에 요청이오면 Keycloak 서버에 해당 토큰의 유효성을 검증
app.get('/hello', keycloak.protect(), function (req, res) {
  res.setHeader('content-type', 'text/plain');
  res.send('Access granted to protected resource');
});

app.listen(8001, function () {
  console.log('Started at port 8001');
});

keycloak.protect()를 사용하여 특정 경로에 대한 접근을 보호하며, 작동방식은 아래와 같다.

  1. AccessToken 수신: 클라이언트가 요청을 보낼 때, Authorization 헤더에 AccessToken을 포함한다.
  2. 토큰 검증: keycloak.protect() 미들웨어가 요청을 가로채고, Keycloak 서버에 해당 토큰의 유효성을 검증한다.
  3. 인증 처리: 토큰이 유효하면 요청을 처리하고, 그렇지 않으면 접근이 거부된다.
728x90