Skip to content
이 페이지는 중국어 원문에서 번역되었으나 이후 원문이 변경되어 내용이 오래되었을 수 있습니다. 중국어 버전이 정본입니다. 중국어 원문 보기 →

OIDC / OAuth2 통합 출시됨 ​

Lurus 통합 신원 인증은 Casdoor 기반이며, 표준 OIDC / OAuth2 인터페이스를 외부에 노출합니다. 애플리케이션이 표준 OIDC를 지원하면 핵심 인증 로직을 수정하지 않고도 Lurus SSO에 바로 연동할 수 있습니다.

1Discovery URL 자동 발견
9표준 엔드포인트
5Grant Type / Flow
S256PKCE 강제 방식

빠른 시작 ​

대다수의 OIDC SDK는 Discovery를 지원하므로, URL 하나만으로 모든 엔드포인트, 알고리즘, 기능을 자동으로 가져올 수 있습니다.

Discovery URL: https://identity.lurus.cn/.well-known/openid-configuration

SDK를 초기화할 때 엔드포인트를 하드코딩하는 대신 이 URL을 직접 가리키면, 서버 측 키 회전이나 엔드포인트 변경 시에도 애플리케이션을 수정할 필요가 없습니다.

최소 구성

client_id + redirect_uri + Discovery URL — 이 세 가지만 갖추면 인가 코드 플로우를 시작할 수 있습니다.

표준 엔드포인트 ​

모든 엔드포인트는 https://identity.lurus.cn을 Base URL로 사용합니다.

엔드포인트 이름경로HTTP 메서드용도
Discovery/.well-known/openid-configurationGET서비스 메타데이터, SDK가 모든 엔드포인트와 알고리즘을 자동으로 읽음
Authorization/oauth/v2/authorizeGET / POST인가 플로우 시작, 로그인 페이지로 리다이렉트
Token/oauth/v2/tokenPOSTaccess_token / id_token / refresh_token 교환
UserInfo/oidc/v1/userinfoGETaccess token으로 현재 사용자 claims 조회
JWKS/oauth/v2/keysGETJWK 공개 키 집합을 가져와 JWT를 로컬에서 검증
Introspection/oauth/v2/introspectPOSTtoken 유효성과 메타 정보 조회(서버 측에서 사용)
Revocation/oauth/v2/revokePOSTaccess / refresh token 취소
End Session/oidc/v1/end_sessionGET / POST로그아웃: Casdoor 세션 종료
Device Authorization/oauth/v2/device_authorizationPOSTDevice Code Flow 시작 엔드포인트

Authorization 엔드포인트 파라미터 ​

파라미터필수설명
client_id✓콘솔에서 애플리케이션 생성 후 획득
redirect_uri✓콘솔에 등록한 URI와 완전히 일치해야 함
response_type✓인가 코드 플로우에서는 고정값 code
scope✓최소 openid 포함, 여러 개는 공백으로 구분
state권장CSRF 방지, 콜백 시 원래 값 그대로 반환
nonce권장재전송 방지, id_token에 기록
code_challengePKCE 필수Base64URL(SHA-256(code_verifier))
code_challenge_methodPKCE 필수고정값 S256
prompt선택login 강제 재로그인; consent 동의 페이지 강제 표시
login_hint선택사용자 이름 미리 채우기, 로그인 가속

지원하는 Grant Type / Flow ​

클라이언트 유형에 따라 인가 플로우를 선택합니다. SPA / Native / Web은 Authorization Code + PKCE를 우선 선택합니다.

Flow적용 시나리오권장 여부
Authorization Code + PKCESPA, Native App, Web App✓ 우선 선택
Client CredentialsM2M, 백엔드 서비스✓ M2M 시나리오
Device CodeCLI, TV, IoT, 브라우저 없는 기기✓ 특수 기기
Refresh Token장기 세션, 무음 갱신offline_access scope와 함께 사용
JWT Bearer(Service User)서비스 계정, 서명된 JWT로 token 교환서비스 계정 시나리오

Authorization Code + PKCE 6단계:

  1. 클라이언트가 code_verifier(랜덤 43-128자)를 생성합니다.
  2. code_challenge = Base64URL(SHA-256(verifier))를 계산합니다.
  3. /oauth/v2/authorize로 리다이렉트합니다(challenge 포함).
  4. 사용자가 로그인하여 인가하면 콜백에 code가 포함됩니다.
  5. /oauth/v2/token에 POST합니다(code + code_verifier 포함).
  6. access / id / refresh token을 획득합니다.

Client Credentials: POST /oauth/v2/token with grant_type=client_credentials + client_id + client_secret + scope=openid urn:casdoor:iam:org:project:id:{projectid}:aud → access_token 획득(id_token 없음, 사용자 신원 없음).

Refresh Token: 최초 인가 scope에 offline_access 포함 → refresh_token을 안전하게 저장 → access_token 만료 시 POST grant_type=refresh_token + refresh_token=<token> → 새 token 획득(refresh_token이 회전될 수 있음).


Authorization Code + PKCE 전체 예제 ​

TypeScript ​

typescript
import { createHash, randomBytes } from "crypto";

// Step 1: 生成 PKCE pair
function generatePKCE() {
  const verifier = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  return { verifier, challenge };
}

// Step 2: 构造 Authorization URL(持久化 verifier + state 到 sessionStorage)
function buildAuthorizeURL(clientId: string, redirectUri: string): string {
  const { verifier, challenge } = generatePKCE();
  sessionStorage.setItem("pkce_verifier", verifier);
  const state = randomBytes(16).toString("base64url");
  sessionStorage.setItem("oauth_state", state);
  const params = new URLSearchParams({
    response_type: "code", client_id: clientId, redirect_uri: redirectUri,
    scope: "openid profile email offline_access", state,
    nonce: randomBytes(16).toString("base64url"),
    code_challenge: challenge, code_challenge_method: "S256",
  });
  return `https://identity.lurus.cn/oauth/v2/authorize?${params}`;
}

// Step 3: 回调处理 — 验证 state,提取 code
function handleCallback(callbackURL: string) {
  const url = new URL(callbackURL);
  const code = url.searchParams.get("code")!;
  const state = url.searchParams.get("state")!;
  if (state !== sessionStorage.getItem("oauth_state")) {
    throw new Error("State mismatch — possible CSRF attack");
  }
  return code;
}

// Step 4: 用 code 换 tokens
async function exchangeCode(code: string, clientId: string, redirectUri: string) {
  const verifier = sessionStorage.getItem("pkce_verifier")!;
  const resp = await fetch("https://identity.lurus.cn/oauth/v2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code", code, redirect_uri: redirectUri,
      client_id: clientId, code_verifier: verifier,
    }),
  });
  if (!resp.ok) throw new Error(`Token exchange failed: ${await resp.text()}`);
  // 获得 { access_token, id_token, refresh_token, expires_in }
  return await resp.json();
}

curl ​

bash
# code_challenge = Base64URL(SHA-256(code_verifier))
CODE_VERIFIER="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | sha256sum | cut -d' ' -f1 | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=')

# 引导用户访问登录 URL
echo "https://identity.lurus.cn/oauth/v2/authorize?response_type=code\
&client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.example.com/callback\
&scope=openid%20profile%20email%20offline_access&state=random_state_value\
&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256"

# 用回调中的 code 换 tokens
curl -s -X POST https://identity.lurus.cn/oauth/v2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE_FROM_CALLBACK" \
  -d "redirect_uri=https://yourapp.example.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" -d "code_verifier=${CODE_VERIFIER}" | jq .
# 响应:{ "access_token", "token_type":"Bearer", "expires_in":43199, "id_token", "refresh_token" }

Scopes 목록 ​

표준 OIDC scopes는 어떤 claim이 반환될지 결정하며, Casdoor 고유 scopes는 audience, 역할, 조직 제약을 제어합니다.

표준 Scopes ​

Scope설명영향받는 Token
openid필수, OIDC 요청을 선언하고 id_token을 반환id_token
profilename, given_name, family_name, preferred_username, locale 획득id_token, userinfo
emailemail, email_verified 획득id_token, userinfo
phonephone_number, phone_number_verified 획득id_token, userinfo
address사용자 주소 정보 획득id_token, userinfo
offline_accessrefresh_token 요청(Authorization Code 플로우에서만 유효)—

Casdoor 고유 Scopes ​

Scope설명영향받는 Token
urn:casdoor:iam:org:project:id:{projectid}:aud지정한 project ID를 access token의 aud에 추가; 서버 측 검증 시 반드시 일치해야 함access_token
urn:casdoor:iam:org:project:id:casdoor:audCasdoor 자체 project ID를 aud에 추가(Casdoor API 접근용)access_token
urn:casdoor:iam:org:projects:rolestoken에 인가된 모든 프로젝트의 역할 목록을 포함id_token, access_token, userinfo
urn:casdoor:iam:org:project:role:{rolekey}특정 역할 claim만 요청, 예: ...:role:adminid_token, access_token
urn:casdoor:iam:org:id:{orgid}사용자가 해당 조직에 속하도록 제한; 조직 간 로그인 강제 격리검증용
urn:casdoor:iam:org:domain:primary:{domain}사용자가 속한 조직의 기본 도메인 제한, 예: ...:primary:lurus.cn검증용
urn:casdoor:iam:user:metadatatoken에 사용자 정의 metadata 포함(Base64 키-값 쌍)id_token, access_token, userinfo
urn:casdoor:iam:user:resourceowner사용자가 속한 조직의 ID, 이름, 기본 도메인 획득id_token, access_token, userinfo
urn:casdoor:iam:org:idp:id:{idp_id}지정한 IdP(WeCom, Feishu)로 바로 이동, IDP 선택 페이지 건너뜀동작 제어

자주 쓰는 조합(Web App): openid profile email offline_access urn:casdoor:iam:org:projects:roles urn:casdoor:iam:org:project:id:{projectid}:aud


Claims 목록 ​

아래 표는 각 claim이 어떤 token에 나타나는지, 그리고 어떤 scope에 의존하는지 표시합니다.

표준 Claims ​

Claim설명id_tokenaccess_tokenuserinfo의존 Scope
sub사용자 고유 ID(Casdoor 내부 ID)✓✓ (JWT)✓항상
issIssuer, 고정값 https://identity.lurus.cn✓✓—항상
audAudience, 애플리케이션 client_id✓✓—항상
exp / iat만료 / 발급 시간(Unix)✓✓—항상
auth_time사용자 실제 로그인 시간✓——항상
nonce재전송 방지 랜덤 값✓——항상(있는 경우)
amr인증 방식, 예: ["pwd"], ["mfa"]✓——항상
name / given_name / family_name전체 이름 / 이름 / 성✓*—✓profile
preferred_username로그인 이름(username@primarydomain)✓—✓profile
locale / updated_at언어 설정(예: zh) / 정보 갱신 시간✓*—✓profile
email / email_verified이메일 / 검증 여부(Boolean)✓*—✓email

✓* = response_type에 id_token이 포함되거나 명시적으로 요청한 경우에만 반환.

Casdoor 고유 Claims ​

Claim설명id_tokenaccess_tokenuserinfo
urn:casdoor:iam:org:project:roles사용자 프로젝트 역할, 구조 { "roleName": { "orgId": "domain" } }✓✓ (JWT)✓
urn:casdoor:iam:org:domain:primary사용자가 속한 조직의 기본 도메인✓✓ (JWT)✓
urn:casdoor:iam:user:metadata사용자 정의 metadata, { "key": "base64value" }✓✓ (JWT)✓
urn:casdoor:iam:user:resourceowner:id / :name / :primary_domain사용자가 속한 조직 ID / 이름 / 기본 도메인✓✓ (JWT)✓

역할 claim 예시:

json
{ "urn:casdoor:iam:org:project:roles": { "admin": { "178204173316174381": "lurus.cn" }, "viewer": { "178204173316174381": "lurus.cn" } } }

Metadata claim 예시(value는 Base64이며 사용 시 atob() / base64.StdEncoding.DecodeString()으로 디코딩 필요):

json
{ "urn:casdoor:iam:user:metadata": { "department": "ZW5naW5lZXJpbmc=", "employee_id": "VTEwMDEy" } }

Token 검증 ​

서버는 Bearer token을 받은 후 형식만으로 유효성을 판단해서는 안 되며, 반드시 다음을 수행해야 합니다:

1. 从 JWKS 拉公钥(建议缓存 TTL 1小时): GET https://identity.lurus.cn/oauth/v2/keys
2. 用匹配 kid 的公钥验证 JWT 签名
3. 校验标准 claims:iss == "https://identity.lurus.cn";aud 含本应用 client_id 或 project_id;
   exp > now();nbf <= now()(如有)
4. 按需校验业务 claims(角色、组织 ID)

권장 라이브러리 ​

언어라이브러리설치
TS / Node.jsoidc-client-tsbun add oidc-client-ts
TS / Node.jsjose(경량 검증)bun add jose
Gogo-oidcgo get github.com/coreos/go-oidc/v3
Pythonpython-josepip install python-jose
Elixir / Phoenixlurus-phoenix(플랫폼 내부 라이브러리)2l-bs-admin 참조

Go 검증 예제 ​

go
package auth

import (
    "context"
    "fmt"
    "github.com/coreos/go-oidc/v3/oidc"
)

var provider *oidc.Provider

func Init(ctx context.Context) error {
    var err error
    // SDK 自动从 Discovery URL 加载配置和 JWKS
    provider, err = oidc.NewProvider(ctx, "https://identity.lurus.cn")
    return err
}

func VerifyAccessToken(ctx context.Context, rawToken, clientID string) (*oidc.IDToken, error) {
    verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
    token, err := verifier.Verify(ctx, rawToken)
    if err != nil {
        return nil, fmt.Errorf("token verification failed: %w", err)
    }
    return token, nil
}

Introspection(불투명 token) ​

access token이 불투명 형식(JWT가 아님)인 경우 Introspection으로 검증합니다:

bash
curl -X POST https://identity.lurus.cn/oauth/v2/introspect \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d "token=ACCESS_TOKEN_TO_CHECK"
# 响应:{ "active": true, "sub": "...", "exp": 1234567890, ... } 或 { "active": false }

Device Authorization Flow ​

브라우저 입력이 없는 기기(CLI, TV, IoT)에 적합합니다. Lurus CLI 제품(Lumen, kova-cli)은 모두 이 플로우를 사용합니다.

시퀀스:

  1. 기기가 /device_authorization에 POST합니다.
  2. device_code + user_code + verification_uri를 수신합니다.
  3. user_code와 URL을 사용자에게 표시합니다.
  4. 사용자가 브라우저에서 verification_uri를 열고 user_code를 입력하여 로그인 및 인가합니다.
  5. 기기는 interval초마다 /token을 폴링합니다.
  6. 사용자가 인가한 후 다음 폴링에서 access / id token이 반환됩니다.

Step 1: Device Code 요청 ​

bash
curl -s -X POST https://identity.lurus.cn/oauth/v2/device_authorization \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" -d "scope=openid profile email"

응답:

json
{
  "device_code": "Ag_EE...zo9OA",
  "user_code": "GQWC-FWFK",
  "verification_uri": "https://identity.lurus.cn/device",
  "verification_uri_complete": "https://identity.lurus.cn/device?user_code=GQWC-FWFK",
  "expires_in": 300,
  "interval": 5
}

Step 2: 사용자에게 표시 ​

verification_uri(https://identity.lurus.cn/device) + user_code를 표시하거나, verification_uri_complete를 QR 코드로 스캔하게 합니다. 타임아웃은 5분입니다.

Step 3: Token 엔드포인트 폴링 ​

interval초마다 성공하거나 타임아웃될 때까지 폴링합니다. 오류 처리: authorization_pending → 계속 대기; slow_down → 빈도 낮추기; 그 외 → 실패 종료.

bash
while true; do
  RESPONSE=$(curl -s -X POST https://identity.lurus.cn/oauth/v2/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
    -d "device_code=Ag_EE...zo9OA" -d "client_id=YOUR_CLIENT_ID")
  ERROR=$(echo "$RESPONSE" | jq -r '.error // empty')
  if [ -z "$ERROR" ]; then echo "授权成功!"; echo "$RESPONSE" | jq .; break
  elif [ "$ERROR" = "authorization_pending" ]; then sleep 5
  elif [ "$ERROR" = "slow_down" ]; then sleep 10
  else echo "授权失败:$ERROR"; break; fi
done

TypeScript 구현도 동형입니다: fetch로 /device_authorization에 POST하여 flow를 시작하고, setTimeout(interval*1000)으로 /token을 폴링하며, authorization_pending이면 계속하고, slow_down이면 interval을 키우고, resp.ok이면 tokens를 반환합니다.


자주 묻는 질문 ​

가장 흔한 연동 오류와 한 번에 해결하는 방법입니다.

audience 오류(aud claim 불일치)

증상: 검증 시 token audience mismatch / invalid audience 보고. 원인: access token의 aud에는 기본적으로 client_id만 포함됩니다. 해결: scope에 urn:casdoor:iam:org:project:id:{projectid}:aud를 추가하여 project ID를 aud에 명시적으로 기록합니다.

roles claim이 비어 있거나 누락됨

원인: 사용자가 해당 Project에 User Grant가 없거나 역할 scope를 요청하지 않았습니다. 점검: ① 콘솔 Project → Authorizations에서 역할 Grant가 있는지 확인 ② scope에 urn:casdoor:iam:org:projects:roles 포함 ③ Project 설정에서 「Assert Roles on Authentication」 활성화.

id_token에 email이 없음

원인: scope에서 email이 누락되었습니다. 해결: scope에 email을 추가합니다(예: openid profile email).

Refresh token이 만료되어 재로그인 필요

갱신 시 invalid_grant 반환. 가능한 원인: 최초 인가 scope에 offline_access가 없음; refresh token 만료; 사용자가 콘솔에서 세션을 취소함. 해결: Authorization Code 플로우를 다시 시작합니다; 장기 갱신이 필요하면 scope에 offline_access가 포함되고 애플리케이션 유형이 「Web」 또는 「Native」인지 확인합니다.

PKCE code_verifier 불일치

/token이 invalid_grant: code verifier mismatch를 반환. 원인: 두 단계 사이에 verifier가 변경되었거나 Base64URL 인코딩이 일치하지 않습니다(= padding 포함 또는 +/ 사용). 해결: base64url을 사용합니다(padding 없음, +/ 대신 -_); SPA에서는 sessionStorage로 페이지 간 verifier를 유지합니다.


다음 단계

관련 링크 ​

Powered by Lurus Technologies