# 놀아:회원 OAuth 2.0 개발자 가이드

## 개요
놀아:회원은 자체 회원가입/로그인과 외부 서비스용 OAuth 2.0 제공 서버를 함께 운영합니다.  
외부 서비스에는 기본적으로 사용자의 이메일을 제공하지 않으며, 아래 정보만 제공합니다.

- `profile.basic`: 서비스별 고유 식별자, 닉네임
- `profile.avatar`: 서비스별 고유 식별자, 닉네임, 프로필 사진 URL
- `service.member_email.read`(추가권한): 회원 이메일 주소. **관리자 승인을 받은 서비스만** 요청할 수 있고, 사용자 동의 시 UserInfo 응답에 `email`로 제공됩니다.

등록된 callback URL이 OAuth 기준값입니다.  
요청에 포함된 `redirect_uri`는 등록값과 정확히 일치해야 하며, 다르면 `invalid_redirect_uri`로 실패합니다.
`sub`는 서비스별로 다르게 발급되는 고유 식별자입니다. 같은 사용자가 다른 서비스에서 로그인하면 서로 다른 `sub`가 나가므로 서비스 간 동일인 여부를 알 수 없습니다.

## 엔드포인트
- `GET /oauth/authorize`
- `POST /oauth/consent`
- `POST /oauth/token`
- `GET /oauth/userinfo`

## Authorization 요청
```text
GET /oauth/authorize
  ?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https://service.example.com/oauth/callback
  &scope=profile.basic profile.avatar
  &state=RANDOM_STATE
  &code_challenge=BASE64URL_SHA256_CODE_VERIFIER
  &code_challenge_method=S256
```

### 필수 조건
- `response_type`는 `code`
- `redirect_uri`는 등록된 callback URL과 정확히 일치
- 서버는 등록된 callback URL을 기준으로 처리하며, 다르면 `invalid_redirect_uri`로 실패
- `scope`는 공백으로 구분, 콤마는 사용하지 않음
- `state`는 CSRF 방지 및 응답 매칭용 랜덤 값
- `code_challenge_method`는 `S256`

## 샘플 요청/응답

### Authorization 요청 예시
```text
GET https://account.nola.kr/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fservice.example.com%2Foauth%2Fcallback&scope=profile.basic%20profile.avatar&state=RANDOM_STATE&code_challenge=BASE64URL_SHA256_CODE_VERIFIER&code_challenge_method=S256
```

### Authorization 성공 후 callback 예시
```text
GET https://service.example.com/oauth/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE
```

### redirect_uri 불일치 실패 예시
```text
GET https://service.example.com/oauth/callback?error=invalid_redirect_uri&error_description=redirect_uri%20does%20not%20match%20registered%20callback%20URL&state=RANDOM_STATE
```

## 동의 화면
첫 로그인 시 사용자는 서비스 이름, 서비스 설명, 요청 scope를 확인하고 동의/거부를 선택합니다.  
표시해야 하는 핵심 문구:

- 어떤 서비스인지
- 어떤 정보가 전달되는지
- 이메일은 전달되지 않는다는 점

## Token 교환
토큰 요청의 `redirect_uri`도 등록된 callback URL과 정확히 일치해야 합니다.  
다르면 `invalid_redirect_uri`로 실패합니다.

```text
POST /oauth/token
grant_type=authorization_code
client_id=CLIENT_ID
client_secret=CLIENT_SECRET
code=AUTHORIZATION_CODE
redirect_uri=https://service.example.com/oauth/callback
code_verifier=ORIGINAL_CODE_VERIFIER
```

### Token 응답 예시
```json
{
  "access_token": "....",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "....",
  "scope": "profile.basic profile.avatar"
}
```

## UserInfo
```text
GET /oauth/userinfo
Authorization: Bearer ACCESS_TOKEN
```

### 응답 필드
- `sub`: 서비스별 고유 식별자
- `nickname`: 닉네임
- `profile_image_url`: `profile.avatar` scope가 있으면 포함
- `email`: `service.member_email.read` 추가권한(관리자 승인+사용자 동의)이 있으면 포함, 없으면 미포함
- `scope`: 승인된 scope 문자열

### UserInfo 응답 예시
```json
{
  "sub": "pws_2G6O...서비스별고유값...",
  "nickname": "nola",
  "profile_image_url": "https://account.nola.kr/uploads/profile/nola.jpg",
  "scope": "profile.basic profile.avatar"
}
```

## PKCE
- `code_verifier`는 외부 서비스가 생성
- `code_challenge = base64url(SHA256(code_verifier))`
- 토큰 요청 시 원본 `code_verifier`를 다시 전달

## 상태값
- `draft`: 초안
- `queued`: 검증 대기
- `pending_callback`: 콜백 대기
- `active`: 사용 가능
- `revoked`: 차단됨

## 연동 순서
1. 서비스 등록
2. 도메인 인증
3. callback URL 등록
4. client_id / client_secret 확보
5. authorize 요청
6. consent 처리
7. token 교환
8. userinfo 호출
