Skip to content

필사 모드: [DevOps] SSO 서버 Mocking 가이드: OAuth2/OIDC 테스트 환경 구축

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

1. SSO/OAuth2/OIDC 기본 개념 복습

1.1 SSO (Single Sign-On)

SSO는 한 번의 인증으로 여러 서비스에 접근할 수 있게 하는 인증 메커니즘이다. 사용자는 IdP(Identity Provider)에서 한 번 로그인하면, 연결된 모든 SP(Service Provider)에 별도 인증 없이 접근할 수 있다.

┌──────┐ 1. 로그인 ┌──────────┐

│ 사용자 │ ──────────────> │ IdP │

└──┬───┘ │(Keycloak)│

│ └────┬─────┘

│ 2. 토큰 발급 │

│ <─────────────────────────┘

│ 3. 토큰으로 접근

├──────────────────> [서비스 A]

├──────────────────> [서비스 B]

└──────────────────> [서비스 C]

1.2 OAuth 2.0 Authorization Code Flow

가장 일반적인 OAuth 2.0 플로우는 Authorization Code Flow다.

┌──────┐ ┌──────────┐ ┌──────────┐

│Client│ │ AuthZ │ │ Resource │

│(App) │ │ Server │ │ Server │

└──┬───┘ └────┬─────┘ └────┬─────┘

│ 1. /authorize │ │

│────────────────────────────>│ │

│ │ 2. 사용자 인증 │

│ │ (로그인 페이지) │

│ 3. Authorization Code │ │

│<────────────────────────────│ │

│ │ │

│ 4. POST /token │ │

│ (code + client_secret) │ │

│────────────────────────────>│ │

│ │ │

│ 5. Access Token + ID Token │ │

│<────────────────────────────│ │

│ │ │

│ 6. API 호출 (Bearer Token) │ │

│─────────────────────────────┼──────────────────────────────>│

│ │ │

│ 7. 리소스 응답 │ │

│<────────────────────────────┼───────────────────────────────│

1.3 OIDC (OpenID Connect) 핵심 엔드포인트

OIDC는 OAuth 2.0 위에 구축된 인증 레이어다. 다음 엔드포인트가 핵심이다.

| 엔드포인트 | 경로 | 용도 |

| ------------- | --------------------------------- | ------------------------- |

| Discovery | /.well-known/openid-configuration | 모든 엔드포인트 정보 제공 |

| Authorization | /authorize | 사용자 인증 및 동의 |

| Token | /token | 토큰 발급/갱신 |

| UserInfo | /userinfo | 사용자 정보 조회 |

| JWKS | /jwks (또는 /certs) | JWT 검증용 공개키 제공 |

| Introspection | /introspect | 토큰 유효성 검사 |

| Revocation | /revoke | 토큰 무효화 |

1.4 JWT (JSON Web Token) 구조

Header.Payload.Signature

Header: {"alg": "RS256", "typ": "JWT", "kid": "key-id-1"}

Payload: {"sub": "user123", "iss": "https://idp.example.com",

"aud": "my-client", "exp": 1710000000, "iat": 1709996400}

Signature: RS256(base64(header) + "." + base64(payload), privateKey)

2. 왜 SSO Mocking이 필요한가

2.1 개발 환경의 문제점

| 문제 | 설명 |

| ---------------- | ------------------------------------------------------- |

| 외부 의존성 | 실제 IdP 서버에 의존하면 네트워크 장애 시 개발 중단 |

| CI/CD 파이프라인 | 자동화 테스트에서 실제 IdP에 로그인할 수 없음 |

| Rate Limit | IdP의 API 호출 제한으로 대량 테스트 불가 |

| 비용 | 상용 IdP(Okta, Auth0 등)는 API 호출당 과금 |

| 토큰 만료 제어 | 실제 IdP에서 토큰 만료 시간을 자유롭게 설정 불가 |

| 엣지 케이스 | 토큰 오류, 만료, 잘못된 서명 등을 시뮬레이션하기 어려움 |

| 멀티 IdP | 여러 IdP를 동시에 테스트하기 어려움 |

2.2 Mocking 전략 개요

┌─────────────────────────────────────────────────────┐

│ SSO Mocking 전략 │

├─────────────────────────────────────────────────────┤

│ │

│ 경량 Mock │ 중량 Mock │ 코드 레벨 │

│ ────────── │ ────────── │ ──────── │

│ mock-oauth2-server │ Keycloak │ @WithMock │

│ WireMock OIDC │ Testcontainers │ mockMvc │

│ oauth2-mock-server │ │ jose JWT │

│ │ │ │

│ 빠른 시작 │ 현실적인 테스트 │ 단위 테스트 │

│ CI/CD 최적 │ 통합 테스트 │ 가장 빠름 │

└─────────────────────────────────────────────────────┘

3. 방법 1: mock-oauth2-server (NAV)

mock-oauth2-server는 노르웨이 NAV(Norwegian Labour and Welfare Administration)에서 개발한 오픈소스 OAuth2/OIDC Mock 서버다. Kotlin으로 작성되었으며 OkHttp MockWebServer 기반이다.

3.1 주요 특징

- 자동 OIDC Discovery 엔드포인트 제공

- 여러 Identity Provider를 하나의 서버에서 시뮬레이션

- 커스텀 토큰 클레임 설정

- Docker 이미지 제공

- JUnit 5 확장 지원

3.2 Docker Compose로 실행

version: '3.8'

services:

mock-oauth2-server:

image: ghcr.io/navikt/mock-oauth2-server:2.1.10

ports:

- "8080:8080"

environment:

- SERVER_PORT=8080

- JSON_CONFIG={

"interactiveLogin": true,

"httpServer": "NettyWrapper",

"tokenCallbacks": [

{

"issuerId": "default",

"tokenExpiry": 3600,

"requestMappings": [

{

"requestParam": "scope",

"match": "openid profile",

"claims": {

"sub": "testuser",

"name": "Test User",

"email": "test@example.com",

"groups": ["admin", "users"]

}

}

]

}

]

}

healthcheck:

test: ["CMD", "wget", "--spider", "http://localhost:8080/default/.well-known/openid-configuration"]

interval: 5s

timeout: 3s

retries: 10

my-application:

build: .

environment:

- OIDC_ISSUER_URL=http://mock-oauth2-server:8080/default

- OIDC_CLIENT_ID=my-client

- OIDC_CLIENT_SECRET=my-secret

depends_on:

mock-oauth2-server:

condition: service_healthy

3.3 Discovery 엔드포인트 확인

OIDC Discovery

curl http://localhost:8080/default/.well-known/openid-configuration | jq .

응답 예시:

{

"issuer": "http://localhost:8080/default",

"authorization_endpoint": "http://localhost:8080/default/authorize",

"token_endpoint": "http://localhost:8080/default/token",

"userinfo_endpoint": "http://localhost:8080/default/userinfo",

"jwks_uri": "http://localhost:8080/default/jwks",

"end_session_endpoint": "http://localhost:8080/default/endsession",

"response_types_supported": ["code", "id_token", "token"],

"subject_types_supported": ["public"],

"id_token_signing_alg_values_supported": ["RS256"]

}

3.4 토큰 발급 테스트

Client Credentials Grant

curl -X POST http://localhost:8080/default/token \

-H "Content-Type: application/x-www-form-urlencoded" \

-d "grant_type=client_credentials" \

-d "client_id=my-client" \

-d "client_secret=my-secret" \

-d "scope=openid profile"

Authorization Code Grant (2단계)

1단계: Authorization Code 획득

curl -v "http://localhost:8080/default/authorize?\

response_type=code&\

client_id=my-client&\

redirect_uri=http://localhost:3000/callback&\

scope=openid+profile&\

state=random-state"

2단계: Code를 Token으로 교환

curl -X POST http://localhost:8080/default/token \

-H "Content-Type: application/x-www-form-urlencoded" \

-d "grant_type=authorization_code" \

-d "code=AUTHORIZATION_CODE_HERE" \

-d "client_id=my-client" \

-d "client_secret=my-secret" \

-d "redirect_uri=http://localhost:3000/callback"

3.5 JUnit 5 통합

class MyServiceTest {

companion object {

val mockOAuth2Server = MockOAuth2Server()

@BeforeAll

@JvmStatic

fun setup() {

mockOAuth2Server.start(port = 8080)

}

@AfterAll

@JvmStatic

fun teardown() {

mockOAuth2Server.shutdown()

}

}

@Test

fun `should validate token`() {

// 커스텀 클레임으로 토큰 생성

val token = mockOAuth2Server.issueToken(

issuerId = "default",

subject = "testuser",

audience = "my-client",

claims = mapOf(

"name" to "Test User",

"email" to "test@example.com",

"roles" to listOf("admin")

)

)

// 생성된 토큰으로 API 호출 테스트

val response = myService.callApi(token.serialize())

assertEquals(200, response.statusCode)

}

}

3.6 여러 IdP 시뮬레이션

IdP 1: 내부 직원용

curl http://localhost:8080/internal/.well-known/openid-configuration

IdP 2: 외부 파트너용

curl http://localhost:8080/partner/.well-known/openid-configuration

IdP 3: 고객용

curl http://localhost:8080/customer/.well-known/openid-configuration

각 issuerId별로 독립적인 OIDC Provider가 자동 생성된다.

4. 방법 2: WireMock으로 OIDC Provider 시뮬레이션

WireMock을 사용하면 OIDC의 각 엔드포인트를 세밀하게 제어할 수 있다.

4.1 RSA 키 페어 생성

RSA 개인키 생성

openssl genrsa -out private_key.pem 2048

공개키 추출

openssl rsa -in private_key.pem -pubout -out public_key.pem

JWK 형식으로 변환 (Node.js 사용)

node -e "

const crypto = require('crypto');

const fs = require('fs');

const pem = fs.readFileSync('public_key.pem', 'utf8');

const key = crypto.createPublicKey(pem);

const jwk = key.export({ format: 'jwk' });

jwk.kid = 'test-key-1';

jwk.use = 'sig';

jwk.alg = 'RS256';

console.log(JSON.stringify({ keys: [jwk] }, null, 2));

" > jwks.json

4.2 OIDC Discovery 엔드포인트 Stub

`mappings/oidc-discovery.json`:

{

"request": {

"method": "GET",

"urlPath": "/.well-known/openid-configuration"

},

"response": {

"status": 200,

"headers": {

"Content-Type": "application/json"

},

"jsonBody": {

"issuer": "http://localhost:8080",

"authorization_endpoint": "http://localhost:8080/authorize",

"token_endpoint": "http://localhost:8080/token",

"userinfo_endpoint": "http://localhost:8080/userinfo",

"jwks_uri": "http://localhost:8080/jwks",

"response_types_supported": ["code", "id_token", "token"],

"subject_types_supported": ["public"],

"id_token_signing_alg_values_supported": ["RS256"],

"scopes_supported": ["openid", "profile", "email"],

"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],

"claims_supported": ["sub", "name", "email", "iss", "aud", "exp", "iat"]

}

}

}

4.3 JWKS 엔드포인트 Stub

`mappings/jwks.json`:

{

"request": {

"method": "GET",

"urlPath": "/jwks"

},

"response": {

"status": 200,

"headers": {

"Content-Type": "application/json"

},

"bodyFileName": "jwks.json"

}

}

4.4 Token 엔드포인트 Stub

`mappings/token.json`:

{

"request": {

"method": "POST",

"urlPath": "/token",

"bodyPatterns": [

{

"contains": "grant_type=client_credentials"

}

]

},

"response": {

"status": 200,

"headers": {

"Content-Type": "application/json"

},

"jsonBody": {

"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3Qta2V5LTEifQ.eyJzdWIiOiJ0ZXN0dXNlciIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImF1ZCI6Im15LWNsaWVudCIsImV4cCI6OTk5OTk5OTk5OSwiaWF0IjoxNzA5OTk2NDAwfQ.SIGNATURE",

"token_type": "Bearer",

"expires_in": 3600,

"scope": "openid profile"

}

}

}

4.5 UserInfo 엔드포인트 Stub

`mappings/userinfo.json`:

{

"request": {

"method": "GET",

"urlPath": "/userinfo",

"headers": {

"Authorization": {

"matches": "Bearer .*"

}

}

},

"response": {

"status": 200,

"headers": {

"Content-Type": "application/json"

},

"jsonBody": {

"sub": "testuser",

"name": "Test User",

"email": "test@example.com",

"email_verified": true,

"groups": ["admin", "developers"]

}

}

}

4.6 Response Templating으로 동적 토큰 발급

WireMock의 Response Templating을 활용하면 요청 파라미터에 따라 동적으로 토큰을 생성할 수 있다.

`mappings/dynamic-token.json`:

{

"request": {

"method": "POST",

"urlPath": "/token"

},

"response": {

"status": 200,

"headers": {

"Content-Type": "application/json"

},

"body": "{\"access_token\": \"mock-token-{{randomValue length=32 type='ALPHANUMERIC'}}\", \"token_type\": \"Bearer\", \"expires_in\": 3600, \"issued_at\": \"{{now}}\"}",

"transformers": ["response-template"]

}

}

5. 방법 3: Keycloak 테스트 컨테이너

5.1 Testcontainers Keycloak 모듈

실제 Keycloak 서버를 Docker 컨테이너로 실행하여 현실적인 통합 테스트를 수행할 수 있다.

**Gradle 의존성:**

testImplementation 'com.github.dasniko:testcontainers-keycloak:3.3.1'

testImplementation 'org.testcontainers:junit-jupiter:1.19.7'

**테스트 코드:**

@Testcontainers

@SpringBootTest

class KeycloakIntegrationTest {

@Container

static KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:24.0")

.withRealmImportFile("test-realm.json");

@DynamicPropertySource

static void configureProperties(DynamicPropertyRegistry registry) {

registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri",

() -> keycloak.getAuthServerUrl() + "/realms/test-realm");

registry.add("spring.security.oauth2.resourceserver.jwt.jwk-set-uri",

() -> keycloak.getAuthServerUrl() + "/realms/test-realm/protocol/openid-connect/certs");

}

@Test

void shouldAuthenticateWithKeycloak() {

// Keycloak에서 토큰 발급

String token = getAccessToken(

keycloak.getAuthServerUrl(),

"test-realm",

"test-client",

"testuser",

"password"

);

// 토큰으로 API 호출

given()

.header("Authorization", "Bearer " + token)

.when()

.get("/api/protected")

.then()

.statusCode(200);

}

private String getAccessToken(String authServerUrl, String realm,

String clientId, String username, String password) {

return RestAssured

.given()

.contentType("application/x-www-form-urlencoded")

.formParam("grant_type", "password")

.formParam("client_id", clientId)

.formParam("username", username)

.formParam("password", password)

.post(authServerUrl + "/realms/" + realm + "/protocol/openid-connect/token")

.then()

.extract()

.path("access_token");

}

}

5.2 Realm Import 파일

`test-realm.json`:

{

"realm": "test-realm",

"enabled": true,

"sslRequired": "none",

"roles": {

"realm": [{ "name": "admin" }, { "name": "user" }]

},

"clients": [

{

"clientId": "test-client",

"enabled": true,

"publicClient": true,

"directAccessGrantsEnabled": true,

"redirectUris": ["http://localhost:*"],

"webOrigins": ["*"]

}

],

"users": [

{

"username": "testuser",

"enabled": true,

"email": "test@example.com",

"firstName": "Test",

"lastName": "User",

"credentials": [

{

"type": "password",

"value": "password",

"temporary": false

}

],

"realmRoles": ["admin", "user"]

}

]

}

6. 방법 4: Spring Security Test

6.1 @WithMockUser

가장 간단한 방법으로, 실제 인증 플로우 없이 Mock 사용자로 테스트한다.

@WebMvcTest(UserController.class)

class UserControllerTest {

@Autowired

private MockMvc mockMvc;

@Test

@WithMockUser(username = "admin", roles = {"ADMIN"})

void adminCanAccessAdminEndpoint() throws Exception {

mockMvc.perform(get("/api/admin/dashboard"))

.andExpect(status().isOk());

}

@Test

@WithMockUser(username = "user", roles = {"USER"})

void userCannotAccessAdminEndpoint() throws Exception {

mockMvc.perform(get("/api/admin/dashboard"))

.andExpect(status().isForbidden());

}

}

6.2 JWT Mock (mockOidcLogin / mockJwt)

@WebMvcTest(ApiController.class)

class JwtSecurityTest {

@Autowired

private MockMvc mockMvc;

@Test

void shouldAccessWithMockJwt() throws Exception {

mockMvc.perform(get("/api/data")

.with(jwt()

.jwt(j -> j

.subject("testuser")

.claim("email", "test@example.com")

.claim("roles", List.of("ADMIN"))

)

.authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))

))

.andExpect(status().isOk());

}

@Test

void shouldAccessWithMockOidcLogin() throws Exception {

mockMvc.perform(get("/api/profile")

.with(oidcLogin()

.idToken(token -> token

.subject("testuser")

.claim("name", "Test User")

.claim("email", "test@example.com")

)

.userInfoToken(userInfo -> userInfo

.claim("groups", List.of("admin"))

)

))

.andExpect(status().isOk());

}

}

7. 방법 5: Node.js에서 jose 라이브러리로 JWT 서명

7.1 jose를 사용한 JWT 생성

const jose = require('jose')

const http = require('http')

// RSA 키 페어 생성

async function createMockOIDCServer(port = 9000) {

const { publicKey, privateKey } = await jose.generateKeyPair('RS256')

const publicJwk = await jose.exportJWK(publicKey)

publicJwk.kid = 'test-key-1'

publicJwk.use = 'sig'

publicJwk.alg = 'RS256'

const issuer = `http://localhost:${port}`

// JWT 토큰 생성 함수

async function createToken(claims = {}) {

const token = await new jose.SignJWT({

sub: 'testuser',

email: 'test@example.com',

name: 'Test User',

...claims,

})

.setProtectedHeader({ alg: 'RS256', kid: 'test-key-1' })

.setIssuer(issuer)

.setAudience('my-client')

.setExpirationTime('1h')

.setIssuedAt()

.sign(privateKey)

return token

}

// HTTP 서버 생성

const server = http.createServer(async (req, res) => {

res.setHeader('Content-Type', 'application/json')

if (req.url === '/.well-known/openid-configuration') {

res.end(

JSON.stringify({

issuer,

authorization_endpoint: `${issuer}/authorize`,

token_endpoint: `${issuer}/token`,

userinfo_endpoint: `${issuer}/userinfo`,

jwks_uri: `${issuer}/jwks`,

})

)

} else if (req.url === '/jwks') {

res.end(JSON.stringify({ keys: [publicJwk] }))

} else if (req.url === '/token' && req.method === 'POST') {

const token = await createToken()

res.end(

JSON.stringify({

access_token: token,

token_type: 'Bearer',

expires_in: 3600,

})

)

} else if (req.url === '/userinfo') {

res.end(

JSON.stringify({

sub: 'testuser',

name: 'Test User',

email: 'test@example.com',

})

)

} else {

res.statusCode = 404

res.end(JSON.stringify({ error: 'not_found' }))

}

})

server.listen(port, () => {

console.log(`Mock OIDC Server running on ${issuer}`)

})

return { server, createToken }

}

// 사용법

createMockOIDCServer(9000).then(async ({ createToken }) => {

const token = await createToken({

roles: ['admin'],

department: 'engineering',

})

console.log('Generated token:', token)

})

7.2 Express 기반 Mock OIDC Server

const express = require('express')

const jose = require('jose')

async function startMockOIDC(port = 9000) {

const app = express()

app.use(express.urlencoded({ extended: true }))

const { publicKey, privateKey } = await jose.generateKeyPair('RS256')

const publicJwk = await jose.exportJWK(publicKey)

publicJwk.kid = 'test-key-1'

publicJwk.use = 'sig'

publicJwk.alg = 'RS256'

const issuer = `http://localhost:${port}`

// Discovery

app.get('/.well-known/openid-configuration', (req, res) => {

res.json({

issuer,

authorization_endpoint: `${issuer}/authorize`,

token_endpoint: `${issuer}/token`,

userinfo_endpoint: `${issuer}/userinfo`,

jwks_uri: `${issuer}/jwks`,

response_types_supported: ['code', 'id_token', 'token'],

subject_types_supported: ['public'],

id_token_signing_alg_values_supported: ['RS256'],

})

})

// JWKS

app.get('/jwks', (req, res) => {

res.json({ keys: [publicJwk] })

})

// Token endpoint

app.post('/token', async (req, res) => {

const token = await new jose.SignJWT({

sub: 'testuser',

email: 'test@example.com',

name: 'Test User',

scope: req.body.scope || 'openid profile',

})

.setProtectedHeader({ alg: 'RS256', kid: 'test-key-1' })

.setIssuer(issuer)

.setAudience(req.body.client_id || 'my-client')

.setExpirationTime('1h')

.setIssuedAt()

.sign(privateKey)

res.json({

access_token: token,

id_token: token,

token_type: 'Bearer',

expires_in: 3600,

})

})

// UserInfo

app.get('/userinfo', (req, res) => {

res.json({

sub: 'testuser',

name: 'Test User',

email: 'test@example.com',

email_verified: true,

})

})

app.listen(port, () => console.log(`Mock OIDC on ${issuer}`))

}

startMockOIDC()

8. 방법 6: Python에서 authlib Mock

test_oauth.py

from unittest.mock import patch, MagicMock

from authlib.jose import jwt

from authlib.jose import JsonWebKey

from cryptography.hazmat.primitives.asymmetric import rsa

from cryptography.hazmat.backends import default_backend

RSA 키 페어 생성

private_key = rsa.generate_private_key(

public_exponent=65537,

key_size=2048,

backend=default_backend()

)

def create_mock_token(claims=None):

"""테스트용 JWT 토큰 생성"""

default_claims = {

"sub": "testuser",

"iss": "http://localhost:9000",

"aud": "my-client",

"exp": 9999999999,

"iat": 1709996400,

"name": "Test User",

"email": "test@example.com",

}

if claims:

default_claims.update(claims)

header = {"alg": "RS256", "kid": "test-key-1"}

token = jwt.encode(header, default_claims, private_key)

return token.decode("utf-8")

class TestOAuthProtectedEndpoint:

def test_authenticated_request(self, client):

token = create_mock_token({"roles": ["admin"]})

response = client.get(

"/api/data",

headers={"Authorization": f"Bearer {token}"}

)

assert response.status_code == 200

def test_expired_token(self, client):

token = create_mock_token({"exp": 1000000000})

response = client.get(

"/api/data",

headers={"Authorization": f"Bearer {token}"}

)

assert response.status_code == 401

def test_missing_token(self, client):

response = client.get("/api/data")

assert response.status_code == 401

9. 실전 팁

9.1 토큰 만료 테스트

// mock-oauth2-server에서 매우 짧은 만료 시간 설정

val token = mockOAuth2Server.issueToken(

issuerId = "default",

subject = "testuser",

expiry = 1 // 1초 후 만료

)

Thread.sleep(2000) // 2초 대기

// 만료된 토큰으로 요청 -> 401 기대

val response = client.get("/api/data") {

header("Authorization", "Bearer ${token.serialize()}")

}

assertEquals(401, response.status.value)

9.2 리프레시 토큰 시나리오

{

"request": {

"method": "POST",

"urlPath": "/token",

"bodyPatterns": [{ "contains": "grant_type=refresh_token" }]

},

"response": {

"status": 200,

"headers": { "Content-Type": "application/json" },

"jsonBody": {

"access_token": "new-access-token",

"refresh_token": "new-refresh-token",

"token_type": "Bearer",

"expires_in": 3600

}

}

}

9.3 잘못된 토큰 시나리오

{

"request": {

"method": "GET",

"urlPath": "/api/protected",

"headers": {

"Authorization": {

"equalTo": "Bearer invalid-token"

}

}

},

"response": {

"status": 401,

"headers": {

"Content-Type": "application/json",

"WWW-Authenticate": "Bearer error=\"invalid_token\""

},

"jsonBody": {

"error": "invalid_token",

"error_description": "The access token is invalid or has expired"

}

}

}

10. CI/CD 통합 예제

10.1 GitHub Actions + mock-oauth2-server

name: Integration Tests with SSO Mock

on: [push, pull_request]

jobs:

test:

runs-on: ubuntu-latest

services:

mock-oauth2:

image: ghcr.io/navikt/mock-oauth2-server:2.1.10

ports:

- 8080:8080

env:

SERVER_PORT: '8080'

options: >-

--health-cmd "wget --spider http://localhost:8080/default/.well-known/openid-configuration"

--health-interval 5s

--health-timeout 3s

--health-retries 10

steps:

- uses: actions/checkout@v4

- name: Setup Java

uses: actions/setup-java@v4

with:

distribution: 'temurin'

java-version: '21'

- name: Run Tests

run: ./gradlew test

env:

OIDC_ISSUER_URL: http://localhost:8080/default

OIDC_CLIENT_ID: test-client

OIDC_JWKS_URI: http://localhost:8080/default/jwks

- name: Upload Test Report

if: always()

uses: actions/upload-artifact@v4

with:

name: test-report

path: build/reports/tests/

10.2 GitLab CI + Keycloak Testcontainer

integration-test:

stage: test

image: gradle:8-jdk21

services:

- docker:dind

variables:

DOCKER_HOST: tcp://docker:2375

DOCKER_TLS_CERTDIR: ''

TESTCONTAINERS_RYUK_DISABLED: 'true'

script:

- gradle integrationTest

artifacts:

reports:

junit: build/test-results/integrationTest/*.xml

11. 도구 선택 가이드

| 상황 | 추천 도구 | 이유 |

| ----------------------------- | ----------------------- | ----------------------------------- |

| 빠른 OIDC Mock 필요 | mock-oauth2-server | Docker 한 줄로 실행, 자동 Discovery |

| OIDC 엔드포인트 세밀 제어 | WireMock | 각 엔드포인트별 커스텀 응답 |

| 현실적인 Keycloak 통합 테스트 | Testcontainers Keycloak | 실제 Keycloak 동작 |

| Spring Boot 단위 테스트 | @WithMockUser / mockJwt | 가장 빠름, 외부 의존성 없음 |

| Node.js 프로젝트 | jose + Express Mock | 경량, 커스터마이징 자유 |

| Python 프로젝트 | authlib Mock | Python 생태계 통합 |

| 멀티 IdP 테스트 | mock-oauth2-server | 여러 issuer 동시 지원 |

12. 결론

SSO Mocking은 현대 마이크로서비스 개발에서 필수적인 테스트 전략이다. 핵심 정리:

- **mock-oauth2-server**는 가장 간편하게 OIDC Mock 환경을 구축할 수 있는 도구

- **WireMock**은 엔드포인트별 세밀한 제어가 필요할 때 적합

- **Keycloak Testcontainers**는 실제에 가장 가까운 통합 테스트 환경 제공

- **Spring Security Test**는 단위 테스트에서 가장 빠르고 간편

- CI/CD 파이프라인에 SSO Mock을 포함시켜 자동화된 인증 테스트를 실행해야 한다

- 토큰 만료, 리프레시, 잘못된 서명 등 엣지 케이스를 반드시 테스트한다

현재 단락 (1/746)

SSO는 한 번의 인증으로 여러 서비스에 접근할 수 있게 하는 인증 메커니즘이다. 사용자는 IdP(Identity Provider)에서 한 번 로그인하면, 연결된 모든 SP(Ser...

작성 글자: 0원문 글자: 19,098작성 단락: 0/746