OIDC Provider

OIDC Provider 플러그인을 사용하면 OpenID Connect (OIDC) provider를 구축하고 관리할 수 있으며, Okta나 Azure AD와 같은 타사 서비스에 의존하지 않고 사용자 인증을 완벽하게 제어할 수 있습니다. 또한 다른 서비스가 귀하의 OIDC provider를 통해 사용자를 인증할 수 있도록 허용합니다.

주요 기능:

  • 클라이언트 등록: OIDC provider로 인증할 클라이언트를 등록합니다.
  • 동적 클라이언트 등록: 클라이언트가 동적으로 등록할 수 있도록 허용합니다.
  • 신뢰할 수 있는 클라이언트: 선택적 동의 우회 옵션과 함께 하드코딩된 신뢰할 수 있는 클라이언트를 구성합니다.
  • Authorization Code Flow: Authorization Code Flow를 지원합니다.
  • Public 클라이언트: SPA, 모바일 앱, CLI 도구 등을 위한 public 클라이언트를 지원합니다.
  • JWKS Endpoint: 클라이언트가 토큰을 검증할 수 있도록 JWKS endpoint를 게시합니다. (완전히 구현되지 않음)
  • Refresh Token: refresh token을 발급하고 refresh_token grant를 사용하여 access token 갱신을 처리합니다.
  • OAuth Consent: 사용자 인증을 위한 OAuth 동의 화면을 구현하고, 신뢰할 수 있는 애플리케이션의 경우 동의를 우회할 수 있는 옵션을 제공합니다.
  • UserInfo Endpoint: 클라이언트가 사용자 세부 정보를 검색할 수 있는 UserInfo endpoint를 제공합니다.

이 플러그인은 활발히 개발 중이며 프로덕션 환경에 적합하지 않을 수 있습니다. 문제나 버그는 GitHub에 보고해 주세요.

설치

플러그인 마운트

auth 설정에 OIDC 플러그인을 추가합니다. 플러그인 구성 방법은 OIDC 구성을 참조하세요.

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

const auth = betterAuth({
    plugins: [oidcProvider({
        loginPage: "/sign-in", // 로그인 페이지 경로
        // ...기타 옵션
    })]
})

데이터베이스 마이그레이션

마이그레이션을 실행하거나 스키마를 생성하여 필요한 필드와 테이블을 데이터베이스에 추가합니다.

npx @better-auth/cli migrate
npx @better-auth/cli generate

필드를 수동으로 추가하려면 Schema 섹션을 참조하세요.

클라이언트 플러그인 추가

auth 클라이언트 설정에 OIDC 클라이언트 플러그인을 추가합니다.

import { createAuthClient } from "better-auth/client";
import { oidcClient } from "better-auth/client/plugins"
const authClient = createAuthClient({
    plugins: [oidcClient({
        // OIDC 구성
    })]
})

사용법

설치가 완료되면 OIDC Provider를 사용하여 애플리케이션 내에서 인증 플로우를 관리할 수 있습니다.

새 클라이언트 등록

새로운 OIDC 클라이언트를 등록하려면 oauth2.register 메서드를 사용합니다.

간단한 예제

const application = await client.oauth2.register({
    client_name: "My Client",
    redirect_uris: ["https://client.example.com/callback"],
});

전체 메서드

POST
/oauth2/register
const { data, error } = await authClient.oauth2.register({    redirect_uris: ["https://client.example.com/callback"], // required    token_endpoint_auth_method: "client_secret_basic",    grant_types: ["authorization_code"],    response_types: ["code"],    client_name: "My App",    client_uri: "https://client.example.com",    logo_uri: "https://client.example.com/logo.png",    scope: "profile email",    contacts: ["admin@example.com"],    tos_uri: "https://client.example.com/tos",    policy_uri: "https://client.example.com/policy",    jwks_uri: "https://client.example.com/jwks",    jwks: {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]},    metadata: {"key": "value"},    software_id: "my-software",    software_version: "1.0.0",    software_statement,});
PropDescriptionType
redirect_uris
리디렉션 URI 목록입니다.
string[]
token_endpoint_auth_method?
토큰 endpoint의 인증 메서드입니다.
"none" | "client_secret_basic" | "client_secret_post"
grant_types?
애플리케이션이 지원하는 grant 타입입니다.
("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[]
response_types?
애플리케이션이 지원하는 response 타입입니다.
("code" | "token")[]
client_name?
애플리케이션의 이름입니다.
string
client_uri?
애플리케이션의 URI입니다.
string
logo_uri?
애플리케이션 로고의 URI입니다.
string
scope?
애플리케이션이 지원하는 scope입니다. 공백으로 구분됩니다.
string
contacts?
애플리케이션의 연락처 정보입니다.
string[]
tos_uri?
애플리케이션 서비스 약관의 URI입니다.
string
policy_uri?
애플리케이션 개인정보 보호정책의 URI입니다.
string
jwks_uri?
애플리케이션 JWKS의 URI입니다.
string
jwks?
애플리케이션의 JWKS입니다.
Record<string, any>
metadata?
애플리케이션의 메타데이터입니다.
Record<string, any>
software_id?
애플리케이션의 소프트웨어 ID입니다.
string
software_version?
애플리케이션의 소프트웨어 버전입니다.
string
software_statement?
애플리케이션의 소프트웨어 statement입니다.
string

이 endpoint는 RFC7591 호환 클라이언트 등록을 지원합니다.

애플리케이션이 생성되면 사용자에게 표시할 수 있는 client_idclient_secret을 받게 됩니다.

신뢰할 수 있는 클라이언트

자사 애플리케이션 및 내부 서비스의 경우, OIDC provider 구성에서 직접 신뢰할 수 있는 클라이언트를 구성할 수 있습니다. 신뢰할 수 있는 클라이언트는 더 나은 성능을 위해 데이터베이스 조회를 우회하며 향상된 사용자 경험을 위해 선택적으로 동의 화면을 건너뛸 수 있습니다.

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

const auth = betterAuth({
    plugins: [
      oidcProvider({
        loginPage: "/sign-in",
        trustedClients: [
            {
                clientId: "internal-dashboard",
                clientSecret: "secure-secret-here",
                name: "Internal Dashboard",
                type: "web",
                redirectURLs: ["https://dashboard.company.com/auth/callback"],
                disabled: false,
                skipConsent: true, // 신뢰할 수 있는 클라이언트의 동의 건너뛰기
                metadata: { internal: true }
            },
            {
                clientId: "mobile-app",
                clientSecret: "mobile-secret",
                name: "Company Mobile App",
                type: "native",
                redirectURLs: ["com.company.app://auth"],
                disabled: false,
                skipConsent: false, // 필요한 경우 여전히 동의 요구
                metadata: {}
            }
        ]
    })]
})

UserInfo Endpoint

OIDC Provider에는 클라이언트가 인증된 사용자에 대한 정보를 검색할 수 있는 UserInfo endpoint가 포함되어 있습니다. 이 endpoint는 /oauth2/userinfo에서 사용할 수 있으며 유효한 access token이 필요합니다.

GET
/oauth2/userinfo
client-app.ts
// 클라이언트가 UserInfo endpoint를 사용하는 방법의 예
const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
  headers: {
    'Authorization': 'Bearer ACCESS_TOKEN'
  }
});

const userInfo = await response.json();
// userInfo에는 부여된 scope에 따른 사용자 세부 정보가 포함됩니다

UserInfo endpoint는 인증 중에 부여된 scope에 따라 다른 claim을 반환합니다:

  • openid scope 포함: 사용자 ID(sub claim) 반환
  • profile scope 포함: name, picture, given_name, family_name 반환
  • email scope 포함: email 및 email_verified 반환

getAdditionalUserInfoClaim 함수는 사용자 객체, 요청된 scope 배열 및 클라이언트를 받아, 인증 중에 부여된 scope에 따라 조건부로 claim을 포함할 수 있습니다. 이러한 추가 claim은 UserInfo endpoint 응답과 ID token 모두에 포함됩니다.

동의 화면

사용자가 인증을 위해 OIDC provider로 리디렉션되면 애플리케이션이 데이터에 액세스하도록 권한을 부여하라는 메시지가 표시될 수 있습니다. 이것을 동의 화면이라고 합니다. 기본적으로 Better Auth는 샘플 동의 화면을 표시합니다. 초기화 중에 consentPage 옵션을 제공하여 동의 화면을 사용자 정의할 수 있습니다.

참고: skipConsent: true가 설정된 신뢰할 수 있는 클라이언트는 동의 화면을 완전히 우회하여 자사 애플리케이션에 원활한 경험을 제공합니다.

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    plugins: [oidcProvider({
        consentPage: "/path/to/consent/page"
    })]
})

플러그인은 consent_code, client_idscope 쿼리 매개변수와 함께 사용자를 지정된 경로로 리디렉션합니다. 이 정보를 사용하여 맞춤 동의 화면을 표시할 수 있습니다. 사용자가 동의하면 oauth2.consent를 호출하여 인증을 완료할 수 있습니다.

POST
/oauth2/consent

동의 endpoint는 동의 코드를 전달하는 두 가지 방법을 지원합니다:

방법 1: URL 매개변수

consent-page.ts
// URL에서 동의 코드 가져오기
const params = new URLSearchParams(window.location.search);

// 요청 본문에 코드를 포함하여 동의 제출
const consentCode = params.get('consent_code');
if (!consentCode) {
	throw new Error('URL 매개변수에서 동의 코드를 찾을 수 없습니다');
}

const res = await client.oauth2.consent({
	accept: true, // 또는 거부하려면 false
	consent_code: consentCode,
});

방법 2: 쿠키 기반

consent-page.ts
// 동의 코드는 서명된 쿠키에 자동으로 저장됩니다
// 동의 결정만 제출하면 됩니다
const res = await client.oauth2.consent({
	accept: true, // 또는 거부하려면 false
	// 쿠키 기반 플로우를 사용할 때는 consent_code가 필요하지 않습니다
});

두 방법 모두 완전히 지원됩니다. URL 매개변수 방법은 모바일 앱 및 타사 컨텍스트에서 잘 작동하며, 쿠키 기반 방법은 웹 애플리케이션에 더 간단한 구현을 제공합니다.

로그인 처리

사용자가 인증을 위해 OIDC provider로 리디렉션되었을 때 이미 로그인하지 않은 경우 로그인 페이지로 리디렉션됩니다. 초기화 중에 loginPage 옵션을 제공하여 로그인 페이지를 사용자 정의할 수 있습니다.

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    plugins: [oidcProvider({
        loginPage: "/sign-in"
    })]
})

사용자 측에서 아무것도 처리할 필요가 없습니다. 새 세션이 생성되면 플러그인이 인증 플로우를 계속 처리합니다.

구성

OIDC 메타데이터

초기화 중에 구성 객체를 제공하여 OIDC 메타데이터를 사용자 정의합니다.

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [oidcProvider({
        metadata: {
            issuer: "https://your-domain.com",
            authorization_endpoint: "/custom/oauth2/authorize",
            token_endpoint: "/custom/oauth2/token",
            // ...기타 맞춤 메타데이터
        }
    })]
})

JWKS Endpoint

OIDC Provider 플러그인은 JWT 플러그인과 통합하여 JWKS endpoint에서 검증 가능한 ID token을 위한 비대칭 키 서명을 제공할 수 있습니다.

플러그인을 OIDC 호환으로 만들려면 /token endpoint를 반드시 비활성화해야 합니다. OAuth에 해당하는 endpoint는 /oauth2/token에 있습니다.

auth.ts
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
    disabledPaths: [
        "/token",
    ],
    plugins: [
        jwt(), // JWT 플러그인을 추가해야 합니다
        oidcProvider({
            useJWTPlugin: true, // JWT 플러그인 통합 활성화
            loginPage: "/sign-in",
            // ... 기타 옵션
        })
    ]
})

useJWTPlugin: false(기본값)일 때 ID token은 애플리케이션 secret으로 서명됩니다.

동적 클라이언트 등록

클라이언트가 동적으로 등록할 수 있도록 하려면 allowDynamicClientRegistration 옵션을 true로 설정하여 이 기능을 활성화할 수 있습니다.

auth.ts
const auth = betterAuth({
    plugins: [oidcProvider({
        allowDynamicClientRegistration: true,
    })]
})

이렇게 하면 클라이언트가 /register endpoint를 사용하여 공개적으로 등록할 수 있습니다.

Schema

OIDC Provider 플러그인은 다음 테이블을 데이터베이스에 추가합니다:

OAuth Application

테이블 이름: oauthApplication

Field NameTypeKeyDescription
idstringOAuth 클라이언트의 데이터베이스 ID
clientIdstring각 OAuth 클라이언트의 고유 식별자
clientSecretstringOAuth 클라이언트의 비밀 키. PKCE를 사용하는 public 클라이언트의 경우 선택 사항입니다.
namestring-OAuth 클라이언트의 이름
redirectURLsstring-쉼표로 구분된 리디렉션 URL 목록
metadatastringOAuth 클라이언트의 추가 메타데이터
typestring-OAuth 클라이언트의 타입 (예: web, mobile)
disabledboolean-클라이언트가 비활성화되었는지 여부를 나타냅니다
userIdstring클라이언트를 소유한 사용자의 ID. (선택 사항)
createdAtDate-OAuth 클라이언트가 생성된 시간의 타임스탬프
updatedAtDate-OAuth 클라이언트가 마지막으로 업데이트된 시간의 타임스탬프

OAuth Access Token

테이블 이름: oauthAccessToken

Field NameTypeKeyDescription
idstringaccess token의 데이터베이스 ID
accessTokenstring-클라이언트에게 발급된 access token
refreshTokenstring-클라이언트에게 발급된 refresh token
accessTokenExpiresAtDate-access token의 만료 날짜
refreshTokenExpiresAtDate-refresh token의 만료 날짜
clientIdstringOAuth 클라이언트의 ID
userIdstring토큰과 연결된 사용자의 ID
scopesstring-부여된 scope의 쉼표로 구분된 목록
createdAtDate-access token이 생성된 시간의 타임스탬프
updatedAtDate-access token이 마지막으로 업데이트된 시간의 타임스탬프

테이블 이름: oauthConsent

Field NameTypeKeyDescription
idstring동의의 데이터베이스 ID
userIdstring동의한 사용자의 ID
clientIdstringOAuth 클라이언트의 ID
scopesstring-동의한 scope의 쉼표로 구분된 목록
consentGivenboolean-동의가 부여되었는지 여부를 나타냅니다
createdAtDate-동의가 부여된 시간의 타임스탬프
updatedAtDate-동의가 마지막으로 업데이트된 시간의 타임스탬프

옵션

allowDynamicClientRegistration: boolean - 동적 클라이언트 등록을 활성화하거나 비활성화합니다.

metadata: OIDCMetadata - OIDC provider 메타데이터를 사용자 정의합니다.

loginPage: string - 사용자 정의 로그인 페이지의 경로입니다.

consentPage: string - 사용자 정의 동의 페이지의 경로입니다.

trustedClients: (Client & { skipConsent?: boolean })[] - provider 옵션에서 직접 구성되는 신뢰할 수 있는 클라이언트의 배열입니다. 이러한 클라이언트는 데이터베이스 조회를 우회하며 선택적으로 동의 화면을 건너뛸 수 있습니다.

getAdditionalUserInfoClaim: (user: User, scopes: string[], client: Client) => Record<string, any> - 추가 사용자 정보 claim을 가져오는 함수입니다.

useJWTPlugin: boolean - true일 때 ID token은 JWT 플러그인의 비대칭 키를 사용하여 서명됩니다. false(기본값)일 때 ID token은 애플리케이션 secret을 사용하여 HMAC-SHA256으로 서명됩니다.

schema: AuthPluginSchema - OIDC provider schema를 사용자 정의합니다.