2단계 인증은 7.0.6 에서 서버측이 갖춰졌지만 인증번호를 입력할 화면이 어느 버전에도
없었다. 그래서 그 설정을 켠 사이트는 관리자를 포함한 전원이 로그인할 수 없었다.
원인은 `POST /api/auth/login` 이 조건에 따라 **다른 형태의 200** 을 돌려준다는 것이다.
평소에는 `{token, user}` 지만 2단계 인증이 켜져 있으면 `{two_factor_required,
challenge_id, ...}` 를 돌려준다. 프론트는 앞의 형태만 선언하고 `response.data.user.language`
를 바로 읽었으므로 그 자리에서 TypeError 가 났고, 영문 원문이 로그인 화면에 그대로 노출됐다.
서버는 정상 응답했으므로 서버 로그에는 아무 흔적도 남지 않는다.
이어서 `setToken(undefined)` 가 `localStorage` 에 문자열 `"undefined"` 를 남겼다.
이 값은 truthy 라 이후 모든 요청이 `Bearer undefined` 로 나가 401 이 되고, 사용자에게는
「세션이 만료되었습니다」로 보인다. 관리자 로그인은 한발 더 나가 `null->isAdmin` 으로
500 이 되어, 설정을 되돌릴 수단까지 함께 사라졌다.
## 구현
- 로그인 응답을 판별 유니온(`LoginResult`)으로 표현하고, 형태를 판별한 뒤에 읽는다.
`ApiClient.setToken` 은 비어 있지 않은 문자열만 저장한다.
- 사용자·관리자 로그인 화면에 인증번호 입력 단계를 추가했다. 같은 카드 안에서 넘어가며
「인증번호 다시 받기」와 「처음부터」를 제공한다. 관리자 판정은 코드 확인에 성공한 뒤에
수행하고, 거부할 때는 그 직전에 발급된 토큰을 회수한다.
- 재발송(`login/two-factor/resend`)은 기존 challenge 를 취소하고 새로 발행한다. 유효한
코드를 여러 개 살려 두면 대입 시도의 표적이 넓어진다.
- 인증번호를 보내지 못하면 401 이 아니라 503 으로 답한다. 자격 증명은 올바른데 401 로
뭉개면 사용자는 비밀번호를 의심하며 같은 시도를 반복하고, 운영자는 메일 설정이 깨진
사실을 알 방법이 없다.
- 공개 본인인증 경로(`identity/verify`·`cancel`)가 로그인 목적의 challenge 를 소진하지
못하도록 403 게이트를 세웠다. 소진되면 그 challenge 로 영영 로그인할 수 없다.
- 로그인 시도 제한 429 응답이 다국어 문구를 싣도록 했다(종전에는 프레임워크 기본 영문).
- 다국어 파라미터에서 파이프 표현식이 평가되지 않아 「유효시간 까지」처럼 값이 빠지던
문제를 함께 고쳤다. 같은 결함이 문의 목록 화면에도 있었다.
## 이번 점검에서 함께 고친 것
- 계정 잠금(423)·발송 실패(503) 응답이 사용자·관리자 컨트롤러에 동일하게 복제돼 있었고
그 주석 자신은 "단일 지점에서 만든다" 고 적혀 있었다. 페이로드에 필드가 하나 추가되면
한쪽만 따라가 같은 실패를 두 화면이 다르게 안내하게 된다 — 트레이트로 통합했다.
- 테스트가 개발자 자신의 사이트 설정을 읽고 있었다. 2단계 인증을 켜 둔 환경에서는 로그인
성공을 전제한 테스트가 503 으로 깨지는데 실패 메시지가 원인을 가리키지도 않는다.
같은 결함군을 위해 이미 존재하던 단일 지점에 그 축을 추가했다.
## 버전
코어 7.0.11 · sirsoft-basic 1.1.4 · sirsoft-admin_basic 1.0.9 ·
번들 일본어팩 3종 · 템플릿 엔진 engine-v1.65.0.
69 KiB
Auth API 레퍼런스
소유: 코어 · 생성:
php artisan api:docgen(실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다.
TL;DR (5초 요약)
1. 이 문서는 실제 API 호출로 실측한 Auth 엔드포인트 레퍼런스입니다
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(curl) + 실측 응답 필드 표 + 응답 예시(envelope)
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
5. 설명(TODO) 칸은 사람이 채웁니다
POST /api/admin/auth/logout
- 라우트명:
api.admin.auth.logout - 컨트롤러:
App\Http\Controllers\Api\Admin\AuthController@logout - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/admin/auth/logout HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
이 엔드포인트는 data 를 반환하지 않습니다 (성공 메시지만 — AuthController@logout 이 success('auth.logout_success') 를 인자 없이 호출하므로 data 는 null).
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "로그아웃이 성공했습니다.",
"data": null
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
설명
현재 관리자의 Sanctum 토큰을 폐기해 로그아웃한다. AuthService::logout() 이 3단계(토큰 삭제 → 세션 무효화 → Auth::logout())를 수행하며, data 는 없고 message 만 auth.logout_success 로 내려온다. 프론트는 응답 후 저장된 Bearer 토큰을 폐기하고 로그인 화면으로 전환한다.
POST /api/admin/auth/refresh
- 라우트명:
api.admin.auth.refresh - 컨트롤러:
App\Http\Controllers\Api\Admin\AuthController@refresh - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/admin/auth/refresh HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user | object | {"uuid":"a26219fc-94a0-4f63-9404-04c2a6ac99e4","name":"최고… |
대상 사용자 정보 객체 (uuid/name/email 등 — user 관계 파생) |
| token | string | 380|ZjZl46uRlhHt53MEhb7XAb0r5lYoDEsXF… |
발급된 API 접근 토큰 평문 (Bearer 토큰으로 사용, 발급 시 1회만 노출) |
| token_type | string | Bearer |
토큰 타입 (일반적으로 Bearer) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"user": {
"uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
"name": "최고관리자",
"nickname": "최고관리자",
"email": "heuristing@gmail.com",
"avatar": null,
"language": "ko",
"language_label": "한국어",
"country": null,
"status": "active",
"status_label": "활성",
"status_variant": "success",
"is_admin": true,
"homepage": null,
"mobile": null,
"phone": null,
"zipcode": null,
"address": null,
"address_detail": null,
"signature": null,
"bio": null,
"last_login_at": "2026-08-04 19:00:10",
"email_verified_at": "2026-07-30 23:37:44",
"timezone": "Asia/Seoul",
"created_at": "2026-07-30 23:37:44",
"updated_at": "2026-08-04 19:00:10",
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": false,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 423 | Locked | 계정이 잠긴 경우. 응답 형태는 로그인 엔드포인트의 423 과 동일하다 (auth.account_locked / auth.account_locked_permanently — errors.locked_until, errors.retry_after_seconds, errors.permanent) |
설명
현재 관리자 토큰을 새 Sanctum 토큰으로 교체한다. AuthService::refreshToken() 이 기존 토큰을 폐기하고 새 토큰을 발급하며, data 에는 새 token 과 user(UserResource) 가 담긴다. 만료 임박 토큰을 재발급하는 용도로, 세션 만료로 재인증이 필요한 경우(토큰 무효)에는 401 auth.unauthenticated 를 반환한다.
재발급도 잠금 검사를 거친다. 유효한 기존 세션이 전제이므로 신규 로그인 우회는 아니지만, 관리자가 계정을 잠근 뒤에도 그 세션이 무기한 연장되면 잠금이 실효를 잃는다. 잠긴 계정의 재발급 요청은 423 으로 차단되며 기존 토큰도 폐기되지 않는다.
GET /api/admin/auth/user
- 라우트명:
api.admin.auth.user - 컨트롤러:
App\Http\Controllers\Api\Admin\AuthController@user - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
GET /api/admin/auth/user HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| uuid | string | a26219fc-94a0-4f63-9404-04c2a6ac99e4 |
외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | 최고관리자 |
사용자 이름 |
| nickname | string | 최고관리자 |
닉네임 |
| string | heuristing@gmail.com |
이메일 주소 | |
| avatar | null | null |
아바타 이미지 URL (User::getAvatarUrl() — 아바타 미설정 시 null) |
| language | string | ko |
사용자 언어 설정 (ko: 한국어, en: 영어) |
| language_label | string | 한국어 |
언어 코드의 현지화 라벨 (user.language.{code} 번역) |
| country | null | null |
국가 코드 (ISO 3166-1 alpha-2) |
| status | string | active |
계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) |
| status_label | string | 활성 |
상태의 사람이 읽는 라벨 (상태 Enum label() 산물) |
| status_variant | string | success |
상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용) |
| is_admin | boolean | true |
관리자 역할 보유 여부 (User::isAdmin() — 역할 관계 기반 파생) |
| homepage | null | null |
홈페이지 URL |
| mobile | null | null |
휴대폰 번호 |
| phone | null | null |
전화번호 |
| zipcode | null | null |
우편번호 |
| address | null | null |
기본 주소 |
| address_detail | null | null |
상세 주소 |
| signature | null | null |
서명 |
| bio | null | null |
자기소개 |
| last_login_at | string | 2026-08-04 19:00:10 |
last login 일시 |
| email_verified_at | string | 2026-07-30 23:37:44 |
email verified 일시 |
| timezone | string | Asia/Seoul |
사용자 시간대 (예: Asia/Seoul, UTC) |
| roles | array | [{"id":1,"identifier":"admin","name":"관리자"}] |
사용자에게 부여된 역할 목록 (원소 id/identifier/name — roles 관계 파생, name 은 현지화 라벨) |
| permissions | array | [{"id":2,"identifier":"sirsoft-ecommerce.user-products.re… |
연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) |
| created_at | string | 2026-07-30 23:37:44 |
생성 일시 |
| updated_at | string | 2026-08-04 19:00:10 |
최종 수정 일시 |
| is_owner | boolean | true |
현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | {"can_read":true,"can_create":true,"can_update":true,"can… |
현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
"name": "최고관리자",
"nickname": "최고관리자",
"email": "heuristing@gmail.com",
"avatar": null,
"...": "(24개 키 생략, 총 29개)"
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
설명
관리자 레이아웃 전역 부트스트랩 엔드포인트. _admin_base.json 의 data_source(current_user)가 모든 관리자 페이지 진입 시 자동 호출해, 헤더/권한 게이트/is_admin 분기의 기준 사용자 정보를 채운다. 응답에는 roles.permissions 가 eager load 되어 permissions 배열이 함께 내려온다.
인증 계약: auth:sanctum 필요 — Bearer 토큰이 없거나 만료되면 401 을 반환한다(프론트 data_source 의 auth_required: true 에 대응). 이 계약이 프론트 소비의 SSoT 이므로 미들웨어 체인 변경 시 반드시 프론트 auth_required/auth_mode 와 함께 검토한다(이슈 #64).
POST /api/auth/admin/login
- 라우트명:
api.auth.admin.login - 컨트롤러:
App\Http\Controllers\Api\Admin\AuthController@login - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| body | 예 | — | 이메일 주소 | ||
| password | body | string | 예 | — | 비밀번호 |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.login_validation_rules).
요청 예시
POST /api/auth/admin/login HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"email": "user@example.com",
"password": "Password123!"
}
응답 필드 (data 내부)
단건 응답: data 객체의 필드 (AuthService::login() 이 반환한 배열 — user 만 UserResource 로 감싼다).
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user | object | {"uuid":"a234c2b1-…","name":"관리자","is_admin":true, …} |
로그인한 관리자 정보 (UserResource — 필드 전수는 GET /api/admin/auth/user 응답 필드 표와 동일) |
| token | string | 75|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed… |
발급된 Sanctum 접근 토큰 평문 (이후 Authorization: Bearer 헤더로 사용, 발급 시 1회만 노출) |
| token_type | string | Bearer |
토큰 타입 (항상 Bearer — AuthService::login() 이 상수로 반환) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "관리자 로그인이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"status": "active",
"is_admin": true,
"roles": [
{ "id": 1, "identifier": "admin", "name": "관리자" }
],
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
user객체는 지면 절약을 위해 축약했습니다. 실제로는GET /api/admin/auth/user의data와 동일한UserResource필드 전수가 내려옵니다.
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 이메일/비밀번호가 일치하지 않는 경우 (auth.login_failed) |
| 403 | Forbidden | 자격 증명은 맞지만 관리자 역할이 아닌 경우 (auth.admin_required) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (error.errors 에 필드별 메시지) |
| 423 | Locked | 로그인 실패 누적으로 계정이 잠긴 경우 (auth.account_locked — error.locked_until, error.retry_after_seconds 포함) |
| 429 | Too Many Requests | throttle:auth-login 초과 (auth.too_many_attempts) |
| 503 | Service Unavailable | 2단계 인증이 켜져 있고 인증번호를 보내지 못한 경우 (auth.two_factor_delivery_failed) |
설명
관리자 로그인. email/password 검증 후 AuthService::login() 이 인증하고, 인증 사용자가 isAdmin() 이 아니면 403 auth.admin_required 로 거부한다. 성공 시 data.token(Sanctum Bearer) 과 data.user(UserResource) 를 반환한다. 계정 잠금 시 AccountLockedException, 자격 불일치 시 422 검증 오류를 반환한다. 이후 모든 관리자 API 호출은 이 토큰을 Authorization: Bearer 헤더로 실어야 한다.
403 거부는 이미 발급된 세션을 회수한다. AuthService::login() 은 관리자 판정보다 먼저 토큰과 web 세션을 발급하므로, 거부하면서 그대로 두면 관리자가 아닌 사용자가 응답만 403 을 받을 뿐 유효한 세션을 손에 쥔다. 거부 경로는 AuthService::revokeIssuedSession() 으로 그 발급분을 되돌린다.
2단계 인증이 켜져 있는 경우: 사용자 로그인과 동일하게 200 + message: auth.two_factor_required 와 two_factor_required / challenge_id / provider_id / expires_at 을 반환한다(필드 정의는 POST /api/auth/login 의 같은 절 참조). 이 단계에서는 아직 사용자도 토큰도 없으므로 관리자 판정을 하지 않는다 — 판정은 POST /api/auth/admin/login/two-factor 가 코드 확인에 성공한 뒤에 수행한다.
POST /api/auth/admin/login/two-factor
- 라우트명:
api.auth.admin.login.two-factor - 컨트롤러:
App\Http\Controllers\Api\Admin\AuthController@verifyTwoFactor - 인증/권한: 공개 (인증 불필요 — 주체는 challenge 가 식별한다)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| challenge_id | body | string | 예 | uuid | 관리자 로그인 응답이 돌려준 challenge 식별자 |
| code | body | string | 예 | min 4, max 16 | 사용자가 받은 인증 코드 |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.two_factor_validation_rules).
요청 예시
POST /api/auth/admin/login/two-factor HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"challenge_id": "9f1c2f2e-0b3a-4f0a-9a1e-5c1b7f9d2c40",
"code": "135790"
}
응답 필드 (data 내부)
단건 응답: POST /api/auth/admin/login 의 성공 페이로드와 동일하다.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user | object | {"uuid":"a234c2b1-…","is_admin":true, …} |
로그인한 관리자 정보 (UserResource) |
| token | string | 75|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed… |
발급된 Sanctum 접근 토큰 평문 |
| token_type | string | Bearer |
토큰 타입 (항상 Bearer) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "관리자 로그인이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"status": "active",
"is_admin": true,
"is_owner": true
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
user객체는 지면 절약을 위해 축약했습니다. 실제로는UserResource필드 전수가 내려옵니다.
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthorized | 코드가 틀렸거나(auth.two_factor_failed), challenge 의 purpose 가 login 이 아니거나, 확인된 사용자가 없거나 active 상태가 아닌 경우 |
| 403 | Forbidden | 코드 확인은 통과했으나 관리자 역할이 아닌 경우 (auth.admin_required) |
| 422 | Unprocessable Entity | challenge_id/code 형식 위반 |
| 423 | Locked | 로그인 실패 누적으로 계정이 잠긴 경우. 응답 형태는 POST /api/auth/login 의 423 과 동일 |
| 429 | Too Many Requests | throttle:auth-login 초과 (auth.too_many_attempts) |
설명
관리자 로그인의 인증번호 확인 단계. 사용자 경로(POST /api/auth/login/two-factor)와 같은 규칙으로 코드를 확인하고, 확인에 성공한 뒤에 관리자 여부를 판정한다.
AuthService::completeTwoFactor() 는 코드 확인에 성공한 시점에 토큰을 발급하므로, 관리자 판정으로 403 을 돌려줄 때는 반드시 그 발급분을 회수한다(revokeIssuedSession()). 회수하지 않으면 관리자가 아닌 사용자가 응답만 403 을 받을 뿐 유효한 세션을 손에 쥔다.
POST /api/auth/admin/login/two-factor/resend
- 라우트명:
api.auth.admin.login.two-factor.resend - 컨트롤러:
App\Http\Controllers\Api\Admin\AuthController@resendTwoFactor - 인증/권한: 공개 (인증 불필요 — 주체는 challenge 가 식별한다)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| challenge_id | body | string | 예 | uuid | 관리자 로그인 응답이 돌려준 challenge 식별자 |
요청 예시
POST /api/auth/admin/login/two-factor/resend HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"challenge_id": "9f1c2f2e-0b3a-4f0a-9a1e-5c1b7f9d2c40"
}
응답 필드 (data 내부)
POST /api/auth/login/two-factor/resend 와 동일한 형태다.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| two_factor_required | boolean | true |
항상 true |
| challenge_id | string(uuid) | 9f1c2f2e-0b3a-… |
새 challenge 식별자 (이전 값은 취소됨) |
| provider_id | string | g7:core.mail |
코드를 발송한 본인인증 프로바이더 |
| expires_at | string(ISO8601)|null | 2026-09-07T14:03:00+09:00 |
새 challenge 만료 시각 |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "인증번호를 보냈습니다. 받은 번호를 입력해 로그인을 완료해주세요.",
"data": {
"two_factor_required": true,
"challenge_id": "9f1c2f2e-0b3a-4f0a-9a1e-5c1b7f9d2c40",
"provider_id": "g7:core.mail",
"expires_at": "2026-09-07T14:03:00+09:00"
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 403 | Forbidden | challenge 가 식별한 사용자가 관리자 역할이 아닌 경우 (auth.admin_required) |
| 422 | Unprocessable Entity | 재발송 대상이 아닌 challenge (auth.two_factor_invalid_challenge) — 사유는 구분하지 않는다 |
| 423 | Locked | challenge 를 받은 뒤 계정이 잠긴 경우 |
| 429 | Too Many Requests | throttle:auth-login 초과 (auth.too_many_attempts) |
| 503 | Service Unavailable | 새 인증번호를 보내지 못한 경우 (auth.two_factor_delivery_failed) |
설명
관리자 로그인의 인증번호 재발송. 규칙은 POST /api/auth/login/two-factor/resend 와 동일하다 — 기존 challenge 를 취소하고 새로 발행하므로 앞서 받은 인증번호는 통하지 않는다.
이 단계는 토큰을 발급하지 않으므로 회수할 것이 없다. 다만 완료할 수 없는 상대에게 새 인증번호를 계속 보내지는 않으므로, challenge 가 식별한 사용자가 관리자가 아니면 403 auth.admin_required 로 거부한다. 발급 자체를 막는 것이 아니라 재발송만 막는 것이며, 비밀번호 확인 단계(POST /api/auth/admin/login)는 종전대로 관리자 판정 없이 challenge 를 돌려준다.
POST /api/auth/forgot-password
- 라우트명:
api.auth.forgot-password - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@forgotPassword - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| body | 예 | — | 이메일 주소 | ||
| redirect_prefix | body | string | 아니오 | admin |
재설정 링크가 향할 화면 구분값 — admin 전달 시 관리자 재설정 화면, 미지정 시 사용자 화면 |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.forgot_password_validation_rules).
요청 예시
POST /api/auth/forgot-password HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"email": "user@example.com",
"redirect_prefix": "admin"
}
응답 필드 (data 내부)
이 엔드포인트는 data 를 반환하지 않습니다 (성공 메시지만 — AuthController@forgotPassword 가 success('auth.password_reset_email_sent') 를 인자 없이 호출하므로 data 는 null).
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "비밀번호 재설정 이메일이 발송되었습니다.",
"data": null
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우, 또는 등록되지 않은 이메일인 경우 (auth.password_reset_failed — error.errors.email 에 auth.email_not_registered 메시지) |
설명
비밀번호 재설정 메일 발송을 요청한다(공개). email 로 계정을 찾아 재설정 링크 메일을 보내고 message: auth.password_reset_email_sent 를 반환한다. redirect_prefix 는 재설정 링크가 향할 화면을 구분하는 값으로 관리자 흐름(admin_forgot_password.json)에서는 admin 을 전달해 링크가 관리자 재설정 화면을 가리키게 한다(미지정 시 사용자 화면). 계정 열거 방지를 위해 이메일 존재 여부와 무관하게 동일 응답을 주는 것이 원칙이다.
POST /api/auth/login
- 라우트명:
api.auth.login - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@login - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| body | 예 | — | 이메일 주소 | ||
| password | body | string | 예 | — | 비밀번호 |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.login_validation_rules).
요청 예시
POST /api/auth/login HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"email": "user@example.com",
"password": "Password123!"
}
응답 필드 (data 내부)
단건 응답: data 객체의 필드 (AuthService::login() 이 반환한 배열 — user 만 UserResource 로 감싼다).
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user | object | {"uuid":"a234c2b1-…","name":"홍길동","is_admin":false, …} |
로그인한 사용자 정보 (UserResource — 필드 전수는 GET /api/auth/user 응답 필드 표의 기본(코어) 필드와 동일) |
| token | string | 75|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed… |
발급된 Sanctum 접근 토큰 평문 (이후 Authorization: Bearer 헤더로 사용, 발급 시 1회만 노출) |
| token_type | string | Bearer |
토큰 타입 (항상 Bearer) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "로그인이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"language": "ko",
"status": "active",
"is_admin": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
user객체는 지면 절약을 위해 축약했습니다. 실제로는UserResource필드 전수가 내려옵니다.
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 이메일/비밀번호가 일치하지 않거나 계정 상태가 활성이 아닌 경우 (auth.login_failed) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (error.errors 에 필드별 메시지) |
| 423 | Locked | 로그인 실패 누적으로 계정이 잠긴 경우 (auth.account_locked — error.locked_until, error.retry_after_seconds 포함) |
| 429 | Too Many Requests | throttle:auth-login 초과 (auth.too_many_attempts — Retry-After 헤더 동반) |
| 503 | Service Unavailable | 2단계 인증이 켜져 있고 인증번호를 보내지 못한 경우 (auth.two_factor_delivery_failed) |
설명
일반 사용자 로그인(공개). 관리자 로그인과 달리 isAdmin() 검사가 없다. 성공 시 data.token(Sanctum Bearer) 과 data.user 를 반환하며 message: auth.login_success. 계정 잠금 시 423 auth.account_locked 를 잠금 해제까지 남은 정보(errors.locked_until, errors.retry_after_seconds)와 함께 반환한다. 보안 환경설정의 잠금 시간이 0(무한대)이면 무기한 잠금이 되어 423 auth.account_locked_permanently 와 함께 errors.permanent=true, errors.locked_until=null, errors.retry_after_seconds=null 을 반환하며 Retry-After 헤더도 붙지 않는다. 이 경우 해제 수단은 관리자 해제 API(POST /api/admin/users/{user}/unlock) 뿐이다. 프론트 로그인 폼(partials/auth/_register_form.json 인접)에서 소비한다.
2단계 인증이 켜져 있는 경우: 보안 환경설정 security.two_factor_auth 가 켜져 있으면 비밀번호가 맞아도 토큰을 발급하지 않는다. 대신 200 과 함께 message: auth.two_factor_required 및 아래 필드를 반환하며, 클라이언트는 POST /api/auth/login/two-factor 로 코드를 확인해야 로그인이 완료된다.
| 필드 | 타입 | 설명 |
|---|---|---|
two_factor_required |
boolean | 항상 true — 이 응답이 추가 확인 단계임을 나타낸다 |
challenge_id |
string(uuid) | 확인 단계에 그대로 전달할 challenge 식별자 |
provider_id |
string | 코드를 발송한 본인인증 프로바이더 |
expires_at |
string(ISO8601)|null | challenge 만료 시각 |
이 응답에는 data.token 과 data.user 가 없다. 클라이언트는 two_factor_required 를 먼저 판정해야 한다 — 응답 형태가 하나라고 가정하고 data.user.* 를 읽으면 그 자리에서 예외가 나고, data.token 을 그대로 저장하면 "undefined" 문자열이 남아 이후 모든 요청이 401 로 튕긴다(공개 #133). 코어 클라이언트(AuthManager.login())는 LoginResult 판별 유니온으로 두 형태를 구분해 돌려준다.
인증번호를 보내지 못한 경우: 자격 증명은 올바르지만 코드를 전달할 수단이 없으므로 로그인을 완료할 수 없다. 이때는 401(자격 증명 오류)이 아니라 503 auth.two_factor_delivery_failed 를 반환한다 — 401 로 뭉뚱그리면 사용자는 비밀번호를 의심하며 같은 실패를 반복하고, 운영자는 메일 설정이 깨진 사실을 알 방법이 없다. 발송에 실패해도 2단계 인증을 건너뛰고 로그인시키지는 않는다.
POST /api/auth/login/two-factor
- 라우트명:
api.auth.login.two-factor - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@verifyTwoFactor - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| challenge_id | body | string | 예 | — | 로그인 응답이 돌려준 challenge 식별자 |
| code | body | string | 예 | min 4, max 16 | 사용자가 받은 인증 코드 |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.two_factor_validation_rules).
요청 예시
POST /api/auth/login/two-factor HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"challenge_id": "예시값",
"code": "예시값"
}
응답 필드 (data 내부)
단건 응답: data 객체의 필드 (AuthService::completeTwoFactor() 가 로그인 세션을 발급해 반환한 배열 — user 만 UserResource 로 감싼다). 성공 시 페이로드는 일반 로그인(POST /api/auth/login)과 동일하다.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user | object | {"uuid":"a234c2b1-…","name":"홍길동","is_admin":false, …} |
로그인한 사용자 정보 (UserResource — 필드 전수는 GET /api/auth/user 응답 필드 표의 기본(코어) 필드와 동일) |
| token | string | 75|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed… |
발급된 Sanctum 접근 토큰 평문 (이후 Authorization: Bearer 헤더로 사용, 발급 시 1회만 노출) |
| token_type | string | Bearer |
토큰 타입 (항상 Bearer) |
위 문서의 실측이
422로 관측된 것은 유효한 challenge 없이 프로브가 호출됐기 때문이다. 정상 흐름(비밀번호 단계가 돌려준challenge_id+ 올바른 코드)에서는200과 위 페이로드가 반환된다.
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "로그인이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"language": "ko",
"status": "active",
"is_admin": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
user객체는 지면 절약을 위해 축약했습니다. 실제로는UserResource필드 전수가 내려옵니다.
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthorized | 코드가 틀렸거나(auth.two_factor_failed), challenge 의 purpose 가 login 이 아니거나, 확인된 사용자가 없거나 active 상태가 아닌 경우. 세 사유를 같은 응답으로 뭉뚱그린다 — 구분해 내보내면 challenge 유효성 탐색에 쓰인다 |
| 422 | Unprocessable Entity | challenge_id/code 형식 위반 |
| 423 | Locked | 로그인 실패 누적으로 계정이 잠긴 경우. 응답 형태는 POST /api/auth/login 의 423 과 동일하다 (auth.account_locked / 무기한이면 auth.account_locked_permanently — errors.locked_until, errors.retry_after_seconds, errors.permanent) |
| 429 | Too Many Requests | throttle:auth-login 초과 (로그인과 같은 제한을 공유하므로 코드 대입 시도도 함께 억제된다 — auth.too_many_attempts) |
설명
비밀번호 단계가 돌려준 challenge 를 확인해 로그인을 완료한다. 로그인과 동일한 요청 제한(throttle:auth-login)이 걸려 코드 대입 시도도 함께 억제된다.
challenge 의 purpose 가 login 인지 먼저 대조한다 — 대조하지 않으면 회원가입·비밀번호 재설정 등 다른 흐름에서 발급된 challenge 로 로그인할 수 있다. 코드 확인에 성공하기 전에는 어떤 경우에도 토큰이 발급되지 않는다.
계정 잠금은 이 단계에서 다시 검사한다. 세션을 여는 것은 비밀번호 단계가 아니라 이 엔드포인트이므로, challenge 를 받은 뒤 잠긴 계정은 여기서 423 으로 차단된다. 잠기기 전에 발급받은 challenge 를 잠긴 뒤에 완료하는 것만으로 잠금을 우회할 수 없다. 차단은 로그인 완료 훅(core.auth.after_login)보다 앞서므로 실패 횟수·잠금 해제 시각도 초기화되지 않는다.
POST /api/auth/login/two-factor/resend
- 라우트명:
api.auth.login.two-factor.resend - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@resendTwoFactor - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| challenge_id | body | string | 예 | uuid | 로그인 응답이 돌려준 challenge 식별자 |
요청 예시
POST /api/auth/login/two-factor/resend HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"challenge_id": "9f1c2f2e-0b3a-4f0a-9a1e-5c1b7f9d2c40"
}
응답 필드 (data 내부)
단건 응답: data 객체의 필드 (AuthService::resendTwoFactorChallenge() 가 새로 발행한 challenge — POST /api/auth/login 의 2단계 인증 응답과 같은 형태).
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| two_factor_required | boolean | true |
항상 true — 여전히 추가 확인 단계임을 나타낸다 |
| challenge_id | string(uuid) | 9f1c2f2e-0b3a-… |
새 challenge 식별자. 이전 값은 취소되었으므로 반드시 교체해야 한다 |
| provider_id | string | g7:core.mail |
코드를 발송한 본인인증 프로바이더 |
| expires_at | string(ISO8601)|null | 2026-09-07T14:03:00+09:00 |
새 challenge 만료 시각 |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "인증번호를 보냈습니다. 받은 번호를 입력해 로그인을 완료해주세요.",
"data": {
"two_factor_required": true,
"challenge_id": "9f1c2f2e-0b3a-4f0a-9a1e-5c1b7f9d2c40",
"provider_id": "g7:core.mail",
"expires_at": "2026-09-07T14:03:00+09:00"
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 422 | Unprocessable Entity | challenge_id 형식 위반, 또는 재발송 대상이 아닌 challenge (auth.two_factor_invalid_challenge) — 존재하지 않음 / purpose 가 login 이 아님 / 이미 검증·취소·실패 / 만료 / 대상 사용자가 없거나 active 가 아님. 사유를 구분하지 않는다 — 구분해 내보내면 challenge 유효성 탐색에 쓰인다 |
| 423 | Locked | challenge 를 받은 뒤 계정이 잠긴 경우. 응답 형태는 POST /api/auth/login 의 423 과 동일하다 |
| 429 | Too Many Requests | throttle:auth-login 초과 (auth.too_many_attempts) |
| 503 | Service Unavailable | 새 인증번호를 보내지 못한 경우 (auth.two_factor_delivery_failed) |
설명
인증번호를 받지 못했을 때 새 코드를 발행한다. 서버는 기존 challenge 를 취소하고 새로 발행하므로, 앞서 받은 인증번호는 더 이상 통하지 않는다 — 유효한 코드를 여러 개 동시에 살려 두면 대입 시도의 표적이 넓어진다. 클라이언트는 응답의 challenge_id 로 반드시 교체하고 입력란을 비워야 한다.
계정 잠금은 여기서도 다시 검사한다. challenge 를 받은 뒤 잠긴 계정에는 새 코드를 보내지 않는다. 로그인과 같은 요청 제한(throttle:auth-login)이 걸린다.
POST /api/auth/logout
- 라우트명:
api.auth.logout - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@logout - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/auth/logout HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
이 엔드포인트는 data 를 반환하지 않습니다 (성공 메시지만 — AuthController@logout 이 success('auth.logout_success') 를 인자 없이 호출하므로 data 는 null).
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "로그아웃이 성공했습니다.",
"data": null
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
설명
현재 사용자 토큰을 폐기해 로그아웃한다(message: auth.logout_success). 현재 요청에 사용된 토큰만 폐기하며, 모든 기기에서 로그아웃하려면 /api/user/auth/logout-all-devices 를 사용한다.
POST /api/auth/register
- 라우트명:
api.auth.register - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@register - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| name | body | string | 예 | max 255 | 대상의 이름/명칭 |
| nickname | body | string | 아니오 | max 50 | 닉네임 |
| body | string | 예 | max 255 | 이메일 주소 | |
| password | body | string | 예 | — | 비밀번호 |
| mobile | body | string | 아니오 | max 20 | 휴대폰번호 (선택) |
| phone | body | string | 아니오 | max 20 | 전화번호 (선택) |
| language | body | string | 아니오 | ko, en |
언어 코드 |
| agree_terms | body | string | 아니오 | — | 이용약관 동의 (코어 필수 동의 — accepted 규칙, 미동의 시 가입 거부) |
| agree_privacy | body | string | 아니오 | — | 개인정보 처리방침 동의 (코어 필수 동의 — accepted 규칙, 미동의 시 가입 거부) |
| agree_email_subscription | body | boolean | 아니오 | — | 광고성 이메일 수신 동의 (marketing 플러그인 주입, 선택 항목) |
| agree_marketing_consent | body | boolean | 아니오 | — | 마케팅 정보 수신 전체 동의 (marketing 플러그인 주입, 선택 항목) |
| agree_third_party_consent | body | boolean | 아니오 | — | 제3자 정보 제공 동의 (marketing 플러그인 주입, 선택 항목) |
| agree_info_disclosure | body | boolean | 아니오 | — | 개인정보 이용 안내 동의 (marketing 플러그인 주입, 선택 항목) |
| preferred_currency | body | string | 아니오 | — | 선호 결제 통화 (ecommerce 모듈 주입, 가입 시 계정 기본 통화로 저장) |
| preferred_shipping_country | body | string | 아니오 | — | 선호 배송 국가 코드 (ecommerce 모듈 주입, 가입 시 계정 기본 배송 국가로 저장) |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.register_validation_rules).
요청 예시
POST /api/auth/register HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"name": "예시 이름",
"nickname": "예시 이름",
"email": "user@example.com",
"password": "Password123!",
"mobile": "010-1234-5678",
"phone": "010-1234-5678",
"language": "ko",
"agree_terms": "예시값",
"agree_privacy": "예시값",
"agree_email_subscription": true,
"agree_marketing_consent": true,
"agree_third_party_consent": true,
"agree_info_disclosure": true,
"preferred_currency": "예시값",
"preferred_shipping_country": "KR"
}
응답 필드 (data 내부)
단건 응답: data 객체의 필드 (AuthService::register() 이 반환한 배열 — user 만 UserResource 로 감싼다). 성공 시 HTTP 201.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user | object | {"uuid":"a234c2b1-…","name":"홍길동","status":"active", …} |
생성된 사용자 정보 (UserResource. status 는 active, 가입 후 본인인증 정책이 걸린 경우 pending_verification) |
| token | string | 75|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed… |
가입 즉시 발급되는 Sanctum 접근 토큰 평문 (가입 직후 로그인 상태로 이어짐) |
| token_type | string | Bearer |
토큰 타입 (항상 Bearer) |
응답 예시
HTTP/1.1 201
{
"success": true,
"message": "회원가입이 성공했습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"nickname": null,
"email": "apidoc-sample-user@example.com",
"language": "ko",
"status": "active",
"is_admin": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
user객체는 지면 절약을 위해 축약했습니다. 실제로는UserResource필드 전수가 내려옵니다.
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (auth.register_failed — error.errors 에 필드별 메시지) |
| 428 | Precondition Required | 가입 전 본인인증 정책이 매칭되었으나 유효한 verification_token 이 없는 경우 (core.auth.before_register 훅) |
설명
회원가입(공개). 성공 시 201 auth.register_success 와 data.token/data.user 를 반환해 가입 직후 로그인 상태로 이어진다. agree_* 동의 파라미터(약관/개인정보/이메일수신/마케팅/제3자제공/정보공개)는 가입 시점의 동의 이력으로 기록된다 — 그중 agree_email_subscription/agree_marketing_consent/agree_third_party_consent/agree_info_disclosure 및 preferred_currency/preferred_shipping_country 는 marketing·ecommerce 확장이 훅(core.auth.register_validation_rules)으로 주입하는 파라미터로, 해당 확장 비활성 시 무시된다. 검증 실패 시 422 auth.register_failed.
POST /api/auth/reset-password
- 라우트명:
api.auth.reset-password - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@resetPassword - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| token | body | string | 예 | — | 인증/검증 토큰 |
| body | 예 | — | 이메일 주소 | ||
| password | body | string | 예 | — | 비밀번호 |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.reset_password_validation_rules).
요청 예시
POST /api/auth/reset-password HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"token": "{YOUR_TOKEN}",
"email": "user@example.com",
"password": "Password123!"
}
응답 필드 (data 내부)
이 엔드포인트는 data 를 반환하지 않습니다 (성공 메시지만 — AuthController@resetPassword 가 success('auth.password_reset_success') 를 인자 없이 호출하므로 data 는 null).
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "비밀번호가 성공적으로 재설정되었습니다.",
"data": null
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반했거나, 토큰이 유효하지 않거나 만료된 경우 (auth.password_reset_failed — error.errors 에 필드별 메시지) |
설명
비밀번호 재설정을 실제 반영한다(공개). 재설정 메일의 token 과 email, 새 password 를 받아 비밀번호를 갱신하고 message: auth.password_reset_success. 토큰 만료/불일치 등 검증 실패 시 422 auth.password_reset_failed. 반영 전 토큰 유효성만 먼저 확인하려면 /api/auth/validate-reset-token 을 사용한다.
GET /api/auth/user
- 라우트명:
api.auth.user - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@user - 인증/권한:
auth:sanctum
요청 파라미터
요청 파라미터 없음.
요청 예시
GET /api/auth/user HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
응답 필드 (data 내부)
단건 응답: data 객체의 필드.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| uuid | string | a26219fc-94a0-4f63-9404-04c2a6ac99e4 |
외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | 최고관리자 |
사용자 이름 |
| nickname | string | 최고관리자 |
닉네임 |
| string | heuristing@gmail.com |
이메일 주소 | |
| avatar | null | null |
아바타 이미지 URL (User::getAvatarUrl() — 아바타 미설정 시 null) |
| language | string | ko |
사용자 언어 설정 (ko: 한국어, en: 영어) |
| language_label | string | 한국어 |
언어 코드의 현지화 라벨 (user.language.{code} 번역) |
| country | null | null |
국가 코드 (ISO 3166-1 alpha-2) |
| status | string | active |
계정 상태 (active: 활성, inactive: 비활성, blocked: 차단, withdrawn: 탈퇴) |
| status_label | string | 활성 |
상태의 사람이 읽는 라벨 (상태 Enum label() 산물) |
| status_variant | string | success |
상태 표시 색상/스타일 변형 키 (상태 Enum variant() 산물 — UI 배지용) |
| is_admin | boolean | true |
관리자 역할 보유 여부 (User::isAdmin() — 역할 관계 기반 파생) |
| homepage | null | null |
홈페이지 URL |
| mobile | null | null |
휴대폰 번호 |
| phone | null | null |
전화번호 |
| zipcode | null | null |
우편번호 |
| address | null | null |
기본 주소 |
| address_detail | null | null |
상세 주소 |
| signature | null | null |
서명 |
| bio | null | null |
자기소개 |
| last_login_at | string | 2026-08-04 19:00:10 |
last login 일시 |
| email_verified_at | string | 2026-07-30 23:37:44 |
email verified 일시 |
| timezone | string | Asia/Seoul |
사용자 시간대 (예: Asia/Seoul, UTC) |
| modules_count | array | [] |
접근 가능 모듈 수 (modules_count 속성이 로드된 경우에만 포함 — whenLoaded 성격의 조건부 필드) |
| plugins_count | array | [] |
접근 가능 플러그인 수 (plugins_count 속성이 로드된 경우에만 포함) |
| menus_count | array | [] |
접근 가능 메뉴 수 (menus_count 속성이 로드된 경우에만 포함) |
| modules | array | [] |
접근 가능 모듈 목록 (원소 id/name/slug/is_active — modules 관계 로드 시에만 포함) |
| plugins | array | [] |
접근 가능 플러그인 목록 (원소 id/name/slug/is_active — plugins 관계 로드 시에만 포함) |
| menus | array | [] |
접근 가능 메뉴 목록 (원소 id/title/url/is_active — menus 관계 로드 시에만 포함) |
| roles | array | [{"id":1,"identifier":"admin","name":"관리자"}] |
사용자에게 부여된 역할 목록 (원소 id/identifier/name — roles 관계 파생, name 은 현지화 라벨) |
| permissions | array | [{"id":2,"identifier":"sirsoft-ecommerce.user-products.re… |
연결된 권한 목록 (id/identifier/name — 역할 경유 권한 관계 파생) |
| consents | array | [] |
전체 약관 동의 이력 (원소 consent_type/agreed_at/revoked_at — consents 관계 로드 시 포함, 플러그인 참조용) |
| terms_consent | array | [] |
이용약관 동의 정보 (agreed_at — ConsentType::Terms 동의 이력에서 파생, 미동의 시 null) |
| privacy_consent | array | [] |
개인정보 처리방침 동의 정보 (agreed_at — ConsentType::Privacy 동의 이력에서 파생, 미동의 시 null) |
| created_at | string | 2026-07-30 23:37:44 |
생성 일시 |
| updated_at | string | 2026-08-04 19:00:10 |
최종 수정 일시 |
| is_owner | boolean | true |
현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | {"can_read":true,"can_create":true,"can_update":true,"can… |
현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) |
| notify_post_complete | boolean | false |
게시판 새 글 작성 완료 알림 수신 설정 (marketing 플러그인 주입) |
| notify_post_reply | boolean | false |
내 게시글에 답글 달림 알림 수신 설정 (marketing 플러그인 주입) |
| notify_comment | boolean | false |
내 게시글에 댓글 달림 알림 수신 설정 (marketing 플러그인 주입) |
| notify_reply_comment | boolean | false |
내 댓글에 답글 달림 알림 수신 설정 (marketing 플러그인 주입) |
| email_subscription | boolean | false |
광고성 이메일 수신 동의 여부 (marketing 플러그인 주입) |
| email_subscription_at | null | null |
email subscription 일시 (광고성 이메일 수신 동의 시각, 미동의 시 null) |
| marketing_consent | boolean | false |
마케팅 정보 수신 전체 동의 마스터 키 (marketing 플러그인 주입) |
| marketing_consent_at | null | null |
marketing consent 일시 (마케팅 정보 수신 동의 시각, 미동의 시 null) |
| third_party_consent | boolean | false |
제3자 정보 제공 동의 여부 (법적 항목 — marketing 플러그인 주입) |
| third_party_consent_at | null | null |
third party consent 일시 (제3자 정보 제공 동의 시각, 미동의 시 null) |
| info_disclosure | boolean | false |
개인정보 이용 안내 동의 여부 (법적 항목 — marketing 플러그인 주입) |
| info_disclosure_at | null | null |
info disclosure 일시 (개인정보 이용 안내 동의 시각, 미동의 시 null) |
| marketing_consent_enabled | boolean | true |
마케팅 정보 수신 동의 항목 UI 노출 여부 (관리자 활성화 플래그) |
| marketing_consent_terms_slug | string | marketing-terms |
마케팅 정보 수신 동의에 연결된 약관 slug (미설정 시 null) |
| marketing_consent_terms_slug_set | boolean | true |
마케팅 정보 수신 약관 연결 존재 여부 (프론트 링크 표시 판정용) |
| third_party_consent_enabled | boolean | true |
제3자 정보 제공 동의 항목 UI 노출 여부 (관리자 활성화 플래그) |
| third_party_consent_terms_slug | null | null |
제3자 정보 제공 동의에 연결된 약관 slug (미설정 시 null) |
| third_party_consent_terms_slug_set | boolean | false |
제3자 정보 제공 약관 연결 존재 여부 (프론트 링크 표시 판정용) |
| info_disclosure_enabled | boolean | true |
개인정보 이용 안내 동의 항목 UI 노출 여부 (관리자 활성화 플래그) |
| info_disclosure_terms_slug | null | null |
개인정보 이용 안내 동의에 연결된 약관 slug (미설정 시 null) |
| info_disclosure_terms_slug_set | boolean | false |
개인정보 이용 안내 약관 연결 존재 여부 (프론트 링크 표시 판정용) |
| email_subscription_enabled | boolean | true |
광고성 이메일 수신 동의 항목 UI 노출 여부 (관리자 활성화 플래그) |
| email_subscription_terms_slug | null | null |
광고성 이메일 수신 동의에 연결된 약관 slug (미설정 시 null) |
| email_subscription_terms_slug_set | boolean | false |
광고성 이메일 수신 약관 연결 존재 여부 (프론트 링크 표시 판정용) |
| channels | array | [{"key":"email_subscription","label":"광고성 이메일 수신","enable… |
관리자 정의 전체 마케팅 채널 목록 (원소 key/label/enabled/terms_slug — marketing 플러그인 주입) |
| consent_histories | array | [] |
동의 변경 이력 (원소 channel_key/action/source/created_at — marketing 플러그인 주입) |
| ecommerce_mileage | object | {"enabled":false} |
마일리지 정보 (enabled/잔액 — ecommerce 모듈 주입, 모듈 비활성 시 enabled=false) |
| ecommerce_preferred_currency | string | KRW |
선호 결제 통화 (ecommerce 모듈 주입, 미설정 시 null) |
| ecommerce_preferred_shipping_country | null | null |
선호 배송 국가 코드 (ecommerce 모듈 주입, 미설정 시 null) |
| ecommerce_preferred_shipping_country_name | null | null |
선호 배송 국가 이름 (국가 코드에서 현지화 파생 — ecommerce 모듈 주입, 미설정 시 null) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"uuid": "a26219fc-94a0-4f63-9404-04c2a6ac99e4",
"name": "최고관리자",
"nickname": "최고관리자",
"email": "heuristing@gmail.com",
"avatar": null,
"...": "(63개 키 생략, 총 68개)"
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
설명
프론트(사용자) 레이아웃 전역 부트스트랩 엔드포인트. _user_base.json 의 current_user data_source 가 모든 페이지 진입 시 호출한다. 관리자 user 와 달리 응답을 UserResource::toAuthArray() 로 만들어 core.user.filter_resource_data 필터를 적용하므로, marketing 플러그인·ecommerce 모듈이 훅으로 병합한 필드(notify_*, marketing_consent*, channels, ecommerce_* 등)가 함께 내려온다. 이 필드들은 확장 소유이므로 상세 설명은 각 확장 문서를 따른다. 로그인 시 이 응답이 계정 영속 통화를 덮어쓰는 계약(D-LOGIN-CUR)의 출처다.
인증 계약: 이 경로(api.auth.user)는 auth:sanctum 으로 인증이 필수다. 인증 여부와 무관하게 게스트 컨텍스트가 필요한 화면은 optional.sanctum 이 걸린 /api/user/auth/user(api.user.auth.user)를 사용해야 한다 — 프론트 data_source 의 auth_mode: "optional" 이 이 경로에 대응한다. 두 경로의 미들웨어 차이가 곧 auth_required/auth_mode 계약이므로 변경 시 프론트와 함께 검토한다(이슈 #64).
POST /api/auth/validate-reset-token
- 라우트명:
api.auth.validate-reset-token - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@validateResetToken - 인증/권한: 공개 (인증 불필요)
요청 파라미터
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
|---|---|---|---|---|---|
| token | body | string | 예 | — | 인증/검증 토큰 |
| body | 예 | — | 이메일 주소 |
이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (
core.auth.validate_reset_token_messages).
요청 예시
POST /api/auth/validate-reset-token HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
{
"token": "{YOUR_TOKEN}",
"email": "user@example.com"
}
응답 필드 (data 내부)
단건 응답: data 객체의 필드 (AuthService::validateResetToken() 의 반환 배열).
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| valid | boolean | true |
토큰 유효 여부. 성공 응답에서는 항상 true (유효하지 않으면 컨트롤러가 422 로 전환하므로 이 필드가 false 인 200 응답은 없음) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"valid": true
}
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 422 | Unprocessable Entity | 요청 파라미터 검증 위반, 또는 토큰이 유효하지 않거나 만료된 경우 / 미등록 이메일 (auth.reset_token_invalid — error.errors.token 에 사유 메시지) |
설명
비밀번호 재설정 토큰의 유효성만 사전 확인한다(공개, 비밀번호 미변경). 재설정 화면(admin_reset_password.json/auth/reset_password.json) 진입 시 토큰/이메일이 유효한지 먼저 검사해, 만료·위조 링크면 즉시 오류 화면을 보이고 유효하면 새 비밀번호 입력 폼을 노출하는 용도다. 실제 반영은 /api/auth/reset-password 가 담당한다.
POST /api/user/auth/logout
- 라우트명:
api.user.auth.logout - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@logout - 인증/권한:
optional.sanctum(선택적 인증: 회원/비회원 모두 접근) +permission:core.auth.logout
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/user/auth/logout HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
응답 필드 (data 내부)
이 엔드포인트는 data 를 반환하지 않습니다 (성공 메시지만 — AuthController@logout 이 success('auth.logout_success') 를 인자 없이 호출하므로 data 는 null). 공용 POST /api/auth/logout 과 동일한 컨트롤러 메서드.
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "로그아웃이 성공했습니다.",
"data": null
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(core.auth.logout)이 없는 경우 |
설명
/user prefix 그룹의 사용자 로그아웃. 공용 경로 /api/auth/logout 과 동일하게 현재 토큰을 폐기하되, permission:core.auth.logout 권한 게이트를 추가로 통과해야 한다. 세션 시작(start.api.session)이 걸린 공용 경로와 달리 권한 기반 접근 제어가 필요한 흐름에서 사용한다.
POST /api/user/auth/logout-all-devices
- 라우트명:
api.user.auth.logout-all-devices - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@logoutFromAllDevices - 인증/권한:
optional.sanctum(선택적 인증: 회원/비회원 모두 접근) +permission:core.auth.logout
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/user/auth/logout-all-devices HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
응답 필드 (data 내부)
이 엔드포인트는 data 를 반환하지 않습니다 (성공 메시지만 — AuthController@logoutFromAllDevices 가 success('auth.logout_all_devices_success') 를 인자 없이 호출하므로 data 는 null).
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "모든 디바이스에서 로그아웃되었습니다.",
"data": null
}
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(core.auth.logout)이 없는 경우 |
설명
현재 사용자의 모든 Sanctum 토큰을 폐기해 전 기기에서 로그아웃한다(message: auth.logout_all_devices_success). 비밀번호 변경 후 기존 세션 무효화, 계정 도용 대응 등에 사용한다.
POST /api/user/auth/refresh
- 라우트명:
api.user.auth.refresh - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@refresh - 인증/권한:
optional.sanctum(선택적 인증: 회원/비회원 모두 접근) +permission:core.auth.refresh
요청 파라미터
요청 파라미터 없음.
요청 예시
POST /api/user/auth/refresh HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
응답 필드 (data 내부)
단건 응답: data 객체의 필드 (AuthService::refreshToken() 반환 배열 — 공용 POST /api/admin/auth/refresh 와 동일 shape).
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| user | object | {"uuid":"a234c2b1-…","name":"홍길동", …} |
토큰을 갱신한 사용자 정보 (UserResource) |
| token | string | 75|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed… |
새로 발급된 Sanctum 접근 토큰 평문 (기존 토큰은 폐기됨) |
| token_type | string | Bearer |
토큰 타입 (항상 Bearer) |
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"status": "active",
"is_admin": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
}
},
"token": "{MASKED}",
"token_type": "Bearer"
}
}
user객체는 지면 절약을 위해 축약했습니다. 실제로는UserResource필드 전수가 내려옵니다.
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(core.auth.refresh)이 없는 경우 |
| 423 | Locked | 계정이 잠긴 경우. 응답 형태는 로그인 엔드포인트의 423 과 동일하다 (auth.account_locked / auth.account_locked_permanently — errors.locked_until, errors.retry_after_seconds, errors.permanent). 잠긴 계정은 기존 세션으로도 토큰을 연장할 수 없으며, 차단 시 기존 토큰도 폐기되지 않는다 |
설명
/user prefix 그룹의 토큰 갱신. 공용 refresh 와 동작은 같으나 permission:core.auth.refresh 권한 게이트를 추가로 통과해야 한다. 이 그룹(routes/api.php:271)은 optional.sanctum + RefreshTokenExpiration 미들웨어 아래 있어 토큰 만료 정책 갱신과 함께 동작한다.
GET /api/user/auth/user
- 라우트명:
api.user.auth.user - 컨트롤러:
App\Http\Controllers\Api\Auth\AuthController@user - 인증/권한:
optional.sanctum(선택적 인증: 회원/비회원 모두 접근) +permission:core.auth.user
요청 파라미터
요청 파라미터 없음.
요청 예시
GET /api/user/auth/user HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생략 가능)
응답 필드 (data 내부)
단건 응답: data 객체의 필드. 컨트롤러 메서드가 공용 GET /api/auth/user 와 동일한 AuthController@user 이므로, 응답은 UserResource::toAuthArray() 산물(코어 필드 + core.user.filter_resource_data 로 확장이 병합한 필드)로 그 문서의 응답 필드 표와 동일합니다.
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
|---|---|---|---|
| uuid | string | a234c2b1-cde8-437f-b28b-23323be2b98d |
외부 노출용 UUID (URL/API 식별자, 내부 id 비노출) |
| name | string | API 문서 샘플 사용자 |
사용자 이름 |
| nickname | string | null | song.hyunji |
닉네임 (미설정 시 null) |
| string | apidoc-sample-user@example.com |
이메일 주소 | |
| language | string | ko |
사용자 언어 설정 |
| status | string | active |
계정 상태 (active / inactive / blocked / withdrawn / pending_verification) |
| is_admin | boolean | false |
관리자 역할 보유 여부 |
| roles | array | [{"id":2,"identifier":"user","name":"일반회원"}] |
부여된 역할 목록 (id/identifier/name) |
| permissions | array | [{"id":81,"identifier":"sirsoft-ecommerce.user-products.read","name":"상품 조회"}] |
역할 경유 권한 목록 (id/identifier/name) |
| is_owner | boolean | true |
현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | {"can_read":true,"can_update":true, …} |
이 리소스에 수행 가능한 작업 불리언 맵 |
| (확장 병합 필드) | — | notify_*, marketing_consent*, ecommerce_* 등 |
marketing 플러그인·ecommerce 모듈이 core.user.filter_resource_data 필터로 병합 — 전수는 GET /api/auth/user 응답 필드 표 참조 |
필드 전수(코어 + 확장 병합)는
GET /api/auth/user의 응답 필드 표와 동일하므로 그 표를 SSoT 로 참조합니다. 게스트(비인증) 요청은optional.sanctum을 통과하지만request()->user()가 없으므로 이 경로는 인증 사용자 컨텍스트에서만 사용자 객체를 반환합니다.
응답 예시
HTTP/1.1 200
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"nickname": "song.hyunji",
"email": "apidoc-sample-user@example.com",
"language": "ko",
"status": "active",
"is_admin": false,
"roles": [
{ "id": 2, "identifier": "user", "name": "일반회원" }
],
"permissions": [],
"is_owner": true,
"abilities": {
"can_read": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_assign_roles": true
},
"notify_post_complete": false,
"notify_post_reply": false,
"notify_comment": false,
"notify_reply_comment": false,
"ecommerce_mileage": {
"enabled": false
},
"ecommerce_preferred_currency": null,
"ecommerce_preferred_shipping_country": null,
"ecommerce_preferred_shipping_country_name": null
}
}
지면 절약을 위해 축약했습니다. 실제 응답은
GET /api/auth/user의 응답 예시와 동일한 필드 전수를 포함합니다.
에러 응답
| 상태코드 | 의미 | 발생 조건 |
|---|---|---|
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(core.auth.user)이 없는 경우 |
설명
/user prefix 그룹(routes/api.php:271)의 현재 사용자 정보. 이 그룹은 optional.sanctum 미들웨어 아래 있어 비인증(게스트) 요청도 통과하며, 게스트 컨텍스트가 필요한 프론트 화면의 data_source(auth_mode: "optional")가 이 경로를 소비한다. 응답 필드는 인증된 경우 공용 /api/auth/user 와 동일 형태(toAuthArray 병합 포함)이며, 이 경로에는 추가로 permission:core.auth.user 권한 게이트가 걸린다. 실측이 403 으로 제외된 것은 샘플 사용자에 해당 권한이 없었기 때문으로, 응답 shape 은 공용 user 경로를 참조한다.
인증 계약 요약(이슈 #64):
api.auth.user(auth:sanctum, 필수) ↔api.user.auth.user(optional.sanctum, 선택). 프론트data_source의auth_required: true는 전자에,auth_mode: "optional"은 후자에 대응한다. 어느 한쪽 미들웨어를 바꾸면 프론트 소비 계약이 침묵 속에서 깨지므로 반드시 양쪽 문서를 함께 갱신한다.