@fluojs/passport 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ko.md +179 -0
- package/README.md +179 -0
- package/dist/account/account-linking.d.ts +91 -0
- package/dist/account/account-linking.d.ts.map +1 -0
- package/dist/account/account-linking.js +145 -0
- package/dist/adapters/passport-js.d.ts +77 -0
- package/dist/adapters/passport-js.d.ts.map +1 -0
- package/dist/adapters/passport-js.js +230 -0
- package/dist/cookie/cookie-auth-module.d.ts +65 -0
- package/dist/cookie/cookie-auth-module.d.ts.map +1 -0
- package/dist/cookie/cookie-auth-module.js +84 -0
- package/dist/cookie/cookie-auth.d.ts +40 -0
- package/dist/cookie/cookie-auth.d.ts.map +1 -0
- package/dist/cookie/cookie-auth.js +101 -0
- package/dist/cookie/cookie-manager.d.ts +34 -0
- package/dist/cookie/cookie-manager.d.ts.map +1 -0
- package/dist/cookie/cookie-manager.js +93 -0
- package/dist/decorators.d.ts +42 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +95 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/guard.d.ts +39 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +124 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/internal-tokens.d.ts +3 -0
- package/dist/internal-tokens.d.ts.map +1 -0
- package/dist/internal-tokens.js +4 -0
- package/dist/metadata.d.ts +6 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +105 -0
- package/dist/module.d.ts +36 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +63 -0
- package/dist/refresh/jwt-refresh-token-adapter.d.ts +30 -0
- package/dist/refresh/jwt-refresh-token-adapter.d.ts.map +1 -0
- package/dist/refresh/jwt-refresh-token-adapter.js +103 -0
- package/dist/refresh/refresh-token.d.ts +96 -0
- package/dist/refresh/refresh-token.d.ts.map +1 -0
- package/dist/refresh/refresh-token.js +178 -0
- package/dist/scope.d.ts +5 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +44 -0
- package/dist/status.d.ts +36 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +173 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fluo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# @fluojs/passport
|
|
2
|
+
|
|
3
|
+
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
|
|
5
|
+
fluo를 위한 인증 실행 계층으로, 어떤 `AuthStrategy`든 공통 `AuthGuard`를 통해 요청 컨텍스트(`requestContext.principal`)에 연결합니다.
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [설치](#설치)
|
|
10
|
+
- [사용 시점](#사용-시점)
|
|
11
|
+
- [빠른 시작](#빠른-시작)
|
|
12
|
+
- [일반적인 패턴](#일반적인-패턴)
|
|
13
|
+
- [공개 API 개요](#공개-api-개요)
|
|
14
|
+
- [관련 패키지](#관련-패키지)
|
|
15
|
+
- [예제 소스](#예제-소스)
|
|
16
|
+
|
|
17
|
+
## 설치
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/passport
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 사용 시점
|
|
24
|
+
|
|
25
|
+
- 애플리케이션에 인증 및 권한 부여(RBAC/Scopes) 기능을 추가해야 할 때.
|
|
26
|
+
- 하나의 애플리케이션에서 여러 인증 방식(JWT, 쿠키, API 키 등)을 혼합하여 사용할 때.
|
|
27
|
+
- 기존 Passport.js 전략들을 fluo의 DI 및 비동기 환경에서 재사용하고 싶을 때.
|
|
28
|
+
- 리프레시 토큰 로테이션이나 계정 연결 정책을 구현할 때.
|
|
29
|
+
|
|
30
|
+
## 빠른 시작
|
|
31
|
+
|
|
32
|
+
### 1. 모듈 등록
|
|
33
|
+
|
|
34
|
+
사용할 전략을 정의하고 `PassportModule.forRoot(...)`를 통해 등록합니다.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Module } from '@fluojs/core';
|
|
38
|
+
import { PassportModule } from '@fluojs/passport';
|
|
39
|
+
import { MyJwtStrategy } from './jwt.strategy';
|
|
40
|
+
|
|
41
|
+
@Module({
|
|
42
|
+
imports: [
|
|
43
|
+
PassportModule.forRoot(
|
|
44
|
+
{ defaultStrategy: 'jwt' },
|
|
45
|
+
[{ name: 'jwt', token: MyJwtStrategy }]
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
providers: [MyJwtStrategy],
|
|
49
|
+
})
|
|
50
|
+
export class AuthModule {}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
전략 등록은 `PassportModule.forRoot(...)`로 구성합니다.
|
|
54
|
+
|
|
55
|
+
### 2. 라우트 보호
|
|
56
|
+
|
|
57
|
+
`@UseAuth()`와 `@RequireScopes()`를 사용하여 인증을 강제합니다.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { Controller, Get, type RequestContext } from '@fluojs/http';
|
|
61
|
+
import { UseAuth, RequireScopes } from '@fluojs/passport';
|
|
62
|
+
|
|
63
|
+
@Controller('/profile')
|
|
64
|
+
export class ProfileController {
|
|
65
|
+
@Get('/')
|
|
66
|
+
@UseAuth('jwt')
|
|
67
|
+
@RequireScopes('profile:read')
|
|
68
|
+
async getProfile(input: never, ctx: RequestContext) {
|
|
69
|
+
return { user: ctx.principal };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 일반적인 패턴
|
|
75
|
+
|
|
76
|
+
### Passport.js 브릿지 (Bridge)
|
|
77
|
+
|
|
78
|
+
표준 Passport.js 전략(예: `passport-google-oauth20`)을 fluo의 DI와 비동기 수명 주기에 맞춰 쉽게 변환하여 사용할 수 있습니다.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const googleBridge = createPassportJsStrategyBridge('google', GoogleStrategy, {
|
|
82
|
+
mapPrincipal: ({ user }) => ({ subject: user.id, claims: user }),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 쿠키 인증 프리셋
|
|
87
|
+
|
|
88
|
+
HTTP 쿠키에서 인증 정보를 읽는 애플리케이션이라면 `CookieAuthModule.forRoot(...)`를 사용합니다.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { Module } from '@fluojs/core';
|
|
92
|
+
import {
|
|
93
|
+
CookieAuthModule,
|
|
94
|
+
CookieAuthStrategy,
|
|
95
|
+
COOKIE_AUTH_STRATEGY_NAME,
|
|
96
|
+
PassportModule,
|
|
97
|
+
} from '@fluojs/passport';
|
|
98
|
+
|
|
99
|
+
@Module({
|
|
100
|
+
imports: [
|
|
101
|
+
CookieAuthModule.forRoot(),
|
|
102
|
+
PassportModule.forRoot(
|
|
103
|
+
{ defaultStrategy: COOKIE_AUTH_STRATEGY_NAME },
|
|
104
|
+
[{ name: COOKIE_AUTH_STRATEGY_NAME, token: CookieAuthStrategy }],
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
})
|
|
108
|
+
export class AuthModule {}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
애플리케이션 모듈에서 cookie-auth 지원이 필요하면 `CookieAuthModule.forRoot(...)`를 `PassportModule.forRoot(...)`와 함께 import 하세요.
|
|
112
|
+
|
|
113
|
+
### 리프레시 토큰 수명 주기
|
|
114
|
+
|
|
115
|
+
패키지에서 제공하는 `RefreshTokenStrategy`와 `RefreshTokenService`를 사용하여 안전한 토큰 로테이션 및 폐기 기능을 구현할 수 있습니다.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { Module } from '@fluojs/core';
|
|
119
|
+
import { Controller, Post, type RequestContext } from '@fluojs/http';
|
|
120
|
+
import {
|
|
121
|
+
PassportModule,
|
|
122
|
+
REFRESH_TOKEN_STRATEGY_NAME,
|
|
123
|
+
RefreshTokenModule,
|
|
124
|
+
RefreshTokenStrategy,
|
|
125
|
+
UseAuth,
|
|
126
|
+
} from '@fluojs/passport';
|
|
127
|
+
|
|
128
|
+
@Module({
|
|
129
|
+
imports: [
|
|
130
|
+
RefreshTokenModule.forRoot(MyRefreshTokenService),
|
|
131
|
+
PassportModule.forRoot(
|
|
132
|
+
{ defaultStrategy: REFRESH_TOKEN_STRATEGY_NAME },
|
|
133
|
+
[{ name: REFRESH_TOKEN_STRATEGY_NAME, token: RefreshTokenStrategy }],
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
providers: [MyRefreshTokenService],
|
|
137
|
+
})
|
|
138
|
+
export class AuthModule {}
|
|
139
|
+
|
|
140
|
+
@Controller('/auth')
|
|
141
|
+
export class AuthController {
|
|
142
|
+
@Post('/refresh')
|
|
143
|
+
@UseAuth('refresh-token')
|
|
144
|
+
async refresh(input: never, ctx: RequestContext) {
|
|
145
|
+
return ctx.principal; // 새 토큰 쌍이 포함된 principal 반환
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`RefreshTokenModule.forRoot(...)`를 `PassportModule.forRoot(...)`와 함께 import 하여 refresh-token 전략과 공유 `REFRESH_TOKEN_SERVICE` alias를 같은 모듈 wiring에서 사용하세요.
|
|
151
|
+
|
|
152
|
+
## 공개 API 개요
|
|
153
|
+
|
|
154
|
+
### 데코레이터
|
|
155
|
+
- `@UseAuth(strategyName)`: `AuthGuard`를 부착하고 사용할 전략을 설정합니다.
|
|
156
|
+
- `@RequireScopes(...scopes)`: 특정 권한(스코프) 요구 사항을 강제합니다.
|
|
157
|
+
|
|
158
|
+
### 주요 클래스
|
|
159
|
+
- `PassportModule`: passport 전략 wiring을 위한 모듈 진입점입니다.
|
|
160
|
+
- `AuthGuard`: 전략 체인을 실행하는 HTTP 가드입니다.
|
|
161
|
+
- `CookieAuthModule`: 내장 cookie-auth 프리셋의 모듈 진입점입니다.
|
|
162
|
+
- `CookieManager`: HttpOnly 인증 쿠키 관리를 위한 유틸리티입니다.
|
|
163
|
+
- `RefreshTokenModule`: 내장 refresh-token 프리셋의 모듈 진입점입니다.
|
|
164
|
+
- `JwtRefreshTokenAdapter`: `@fluojs/jwt`의 리프레시 로직을 패스포트 인터페이스로 연결합니다.
|
|
165
|
+
|
|
166
|
+
### 인터페이스
|
|
167
|
+
- `AuthStrategy`: 커스텀 인증 로직 구현을 위한 계약입니다.
|
|
168
|
+
- `AccountLinkPolicy`: 계정 연결 결정 로직을 위한 확장 지점입니다.
|
|
169
|
+
|
|
170
|
+
## 관련 패키지
|
|
171
|
+
|
|
172
|
+
- `@fluojs/jwt`: JWT 기반 전략을 위한 하위 토큰 코어 패키지입니다.
|
|
173
|
+
- `@fluojs/http`: 라우팅 및 가드 인프라를 제공하는 기본 패키지입니다.
|
|
174
|
+
|
|
175
|
+
## 예제 소스
|
|
176
|
+
|
|
177
|
+
- `packages/passport/src/guard.test.ts`: 가드 실행 및 권한 강제 패턴 예제.
|
|
178
|
+
- `packages/passport/src/adapters/passport-js.ts`: Passport.js 브릿지 구현체.
|
|
179
|
+
- `examples/auth-jwt-passport/src/auth/bearer.strategy.ts`: 표준 JWT 전략 구현 예제.
|
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# @fluojs/passport
|
|
2
|
+
|
|
3
|
+
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
|
+
|
|
5
|
+
Strategy-agnostic auth execution layer for fluo. It routes any `AuthStrategy` through a generic `AuthGuard` into the request context, populating `requestContext.principal`.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [When to use](#when-to-use)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Common Patterns](#common-patterns)
|
|
13
|
+
- [Public API](#public-api)
|
|
14
|
+
- [Related Packages](#related-packages)
|
|
15
|
+
- [Example Sources](#example-sources)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/passport
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## When to Use
|
|
24
|
+
|
|
25
|
+
- When you need to protect routes with authentication and authorization (RBAC/Scopes).
|
|
26
|
+
- When using multiple auth strategies (e.g., JWT, Cookies, API Keys) in the same application.
|
|
27
|
+
- When you need a bridge to existing Passport.js strategies.
|
|
28
|
+
- When implementing refresh token rotation or account-linking policies.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Register Modules
|
|
33
|
+
|
|
34
|
+
Define your strategies and register them using `PassportModule.forRoot(...)`.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Module } from '@fluojs/core';
|
|
38
|
+
import { PassportModule } from '@fluojs/passport';
|
|
39
|
+
import { MyJwtStrategy } from './jwt.strategy';
|
|
40
|
+
|
|
41
|
+
@Module({
|
|
42
|
+
imports: [
|
|
43
|
+
PassportModule.forRoot(
|
|
44
|
+
{ defaultStrategy: 'jwt' },
|
|
45
|
+
[{ name: 'jwt', token: MyJwtStrategy }]
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
providers: [MyJwtStrategy],
|
|
49
|
+
})
|
|
50
|
+
export class AuthModule {}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Register strategies through `PassportModule.forRoot(...)`.
|
|
54
|
+
|
|
55
|
+
### 2. Protect Routes
|
|
56
|
+
|
|
57
|
+
Use `@UseAuth()` and `@RequireScopes()` to enforce authentication.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { Controller, Get, type RequestContext } from '@fluojs/http';
|
|
61
|
+
import { UseAuth, RequireScopes } from '@fluojs/passport';
|
|
62
|
+
|
|
63
|
+
@Controller('/profile')
|
|
64
|
+
export class ProfileController {
|
|
65
|
+
@Get('/')
|
|
66
|
+
@UseAuth('jwt')
|
|
67
|
+
@RequireScopes('profile:read')
|
|
68
|
+
async getProfile(input: never, ctx: RequestContext) {
|
|
69
|
+
return { user: ctx.principal };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Common Patterns
|
|
75
|
+
|
|
76
|
+
### Passport.js Bridge
|
|
77
|
+
|
|
78
|
+
Easily adapt any standard Passport.js strategy (like `passport-google-oauth20`) to work with fluo's DI and async lifecycle.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const googleBridge = createPassportJsStrategyBridge('google', GoogleStrategy, {
|
|
82
|
+
mapPrincipal: ({ user }) => ({ subject: user.id, claims: user }),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Cookie Auth Preset
|
|
87
|
+
|
|
88
|
+
Use `CookieAuthModule.forRoot(...)` when your app authenticates requests from HTTP cookies.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { Module } from '@fluojs/core';
|
|
92
|
+
import {
|
|
93
|
+
CookieAuthModule,
|
|
94
|
+
CookieAuthStrategy,
|
|
95
|
+
COOKIE_AUTH_STRATEGY_NAME,
|
|
96
|
+
PassportModule,
|
|
97
|
+
} from '@fluojs/passport';
|
|
98
|
+
|
|
99
|
+
@Module({
|
|
100
|
+
imports: [
|
|
101
|
+
CookieAuthModule.forRoot(),
|
|
102
|
+
PassportModule.forRoot(
|
|
103
|
+
{ defaultStrategy: COOKIE_AUTH_STRATEGY_NAME },
|
|
104
|
+
[{ name: COOKIE_AUTH_STRATEGY_NAME, token: CookieAuthStrategy }],
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
})
|
|
108
|
+
export class AuthModule {}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Import `CookieAuthModule.forRoot(...)` alongside `PassportModule.forRoot(...)` when you want cookie-auth support in an application module.
|
|
112
|
+
|
|
113
|
+
### Refresh Token Lifecycle
|
|
114
|
+
|
|
115
|
+
The package provides a built-in `RefreshTokenStrategy` and `RefreshTokenService` to handle secure token rotation and revocation.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { Module } from '@fluojs/core';
|
|
119
|
+
import { Controller, Post, type RequestContext } from '@fluojs/http';
|
|
120
|
+
import {
|
|
121
|
+
PassportModule,
|
|
122
|
+
REFRESH_TOKEN_STRATEGY_NAME,
|
|
123
|
+
RefreshTokenModule,
|
|
124
|
+
RefreshTokenStrategy,
|
|
125
|
+
UseAuth,
|
|
126
|
+
} from '@fluojs/passport';
|
|
127
|
+
|
|
128
|
+
@Module({
|
|
129
|
+
imports: [
|
|
130
|
+
RefreshTokenModule.forRoot(MyRefreshTokenService),
|
|
131
|
+
PassportModule.forRoot(
|
|
132
|
+
{ defaultStrategy: REFRESH_TOKEN_STRATEGY_NAME },
|
|
133
|
+
[{ name: REFRESH_TOKEN_STRATEGY_NAME, token: RefreshTokenStrategy }],
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
providers: [MyRefreshTokenService],
|
|
137
|
+
})
|
|
138
|
+
export class AuthModule {}
|
|
139
|
+
|
|
140
|
+
@Controller('/auth')
|
|
141
|
+
export class AuthController {
|
|
142
|
+
@Post('/refresh')
|
|
143
|
+
@UseAuth('refresh-token')
|
|
144
|
+
async refresh(input: never, ctx: RequestContext) {
|
|
145
|
+
return ctx.principal; // Contains new token pair
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Import `RefreshTokenModule.forRoot(...)` alongside `PassportModule.forRoot(...)` so the refresh-token strategy and shared `REFRESH_TOKEN_SERVICE` alias are available in the same module wiring.
|
|
151
|
+
|
|
152
|
+
## Public API Overview
|
|
153
|
+
|
|
154
|
+
### Decorators
|
|
155
|
+
- `@UseAuth(strategyName)`: Attaches `AuthGuard` and sets the active strategy.
|
|
156
|
+
- `@RequireScopes(...scopes)`: Enforces specific scope requirements.
|
|
157
|
+
|
|
158
|
+
### Core Classes
|
|
159
|
+
- `PassportModule`: Module entry point for passport strategy wiring.
|
|
160
|
+
- `AuthGuard`: The HTTP guard that executes the strategy chain.
|
|
161
|
+
- `CookieAuthModule`: Module entry point for the built-in cookie-auth preset.
|
|
162
|
+
- `CookieManager`: Utility for managing HttpOnly auth cookies.
|
|
163
|
+
- `RefreshTokenModule`: Module entry point for the built-in refresh-token preset.
|
|
164
|
+
- `JwtRefreshTokenAdapter`: Bridges `@fluojs/jwt` refresh logic to the passport interface.
|
|
165
|
+
|
|
166
|
+
### Interfaces
|
|
167
|
+
- `AuthStrategy`: The contract for implementing custom authentication logic.
|
|
168
|
+
- `AccountLinkPolicy`: Extension point for identity-linking decisions.
|
|
169
|
+
|
|
170
|
+
## Related Packages
|
|
171
|
+
|
|
172
|
+
- `@fluojs/jwt`: The underlying token core for JWT-based strategies.
|
|
173
|
+
- `@fluojs/http`: Provides the routing and guard infrastructure.
|
|
174
|
+
|
|
175
|
+
## Example Sources
|
|
176
|
+
|
|
177
|
+
- `packages/passport/src/guard.test.ts`: Guard execution and scope enforcement patterns.
|
|
178
|
+
- `packages/passport/src/adapters/passport-js.ts`: Implementation of the Passport.js bridge.
|
|
179
|
+
- `examples/auth-jwt-passport/src/auth/bearer.strategy.ts`: JWT strategy implementation.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { FluoError, type MaybePromise } from '@fluojs/core';
|
|
2
|
+
/** DI token for registering an account-linking policy implementation. */
|
|
3
|
+
export declare const ACCOUNT_LINKING_POLICY: unique symbol;
|
|
4
|
+
/** Identity payload from an external authentication provider. */
|
|
5
|
+
export interface AccountIdentity {
|
|
6
|
+
provider: string;
|
|
7
|
+
providerSubject: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
emailVerified?: boolean;
|
|
10
|
+
claims?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** Candidate account that may be linked to an external identity. */
|
|
13
|
+
export interface AccountLinkCandidate {
|
|
14
|
+
accountId: string;
|
|
15
|
+
reason: 'existing-link' | 'email-match' | 'username-match' | string;
|
|
16
|
+
}
|
|
17
|
+
/** Explicit user confirmation payload for a requested linking target. */
|
|
18
|
+
export interface AccountLinkAttempt {
|
|
19
|
+
targetAccountId: string;
|
|
20
|
+
confirmedByUser: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Input passed to account-link policy evaluation. */
|
|
23
|
+
export interface AccountLinkContext {
|
|
24
|
+
identity: AccountIdentity;
|
|
25
|
+
candidates: AccountLinkCandidate[];
|
|
26
|
+
linkAttempt?: AccountLinkAttempt;
|
|
27
|
+
}
|
|
28
|
+
/** Normalized decision contract returned by an `AccountLinkPolicy`. */
|
|
29
|
+
export type AccountLinkPolicyDecision = {
|
|
30
|
+
action: 'link';
|
|
31
|
+
accountId: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
} | {
|
|
34
|
+
action: 'create-account';
|
|
35
|
+
reason: string;
|
|
36
|
+
} | {
|
|
37
|
+
action: 'reject';
|
|
38
|
+
reason: string;
|
|
39
|
+
code?: string;
|
|
40
|
+
} | {
|
|
41
|
+
action: 'conflict';
|
|
42
|
+
reason: string;
|
|
43
|
+
candidateAccountIds: string[];
|
|
44
|
+
};
|
|
45
|
+
/** Policy contract for account-linking decisions. */
|
|
46
|
+
export interface AccountLinkPolicy {
|
|
47
|
+
evaluate(context: AccountLinkContext): MaybePromise<AccountLinkPolicyDecision>;
|
|
48
|
+
}
|
|
49
|
+
/** Optional runtime behavior when no policy is configured. */
|
|
50
|
+
export interface AccountLinkingOptions {
|
|
51
|
+
fallback?: 'create-account' | 'skip';
|
|
52
|
+
}
|
|
53
|
+
/** Final framework-level resolution for account-linking flow. */
|
|
54
|
+
export type AccountLinkingResolution = {
|
|
55
|
+
status: 'linked';
|
|
56
|
+
accountId: string;
|
|
57
|
+
reason: string;
|
|
58
|
+
} | {
|
|
59
|
+
status: 'create-account';
|
|
60
|
+
reason: string;
|
|
61
|
+
} | {
|
|
62
|
+
status: 'skipped';
|
|
63
|
+
reason: string;
|
|
64
|
+
};
|
|
65
|
+
/** Error raised when linking requires user confirmation among candidates. */
|
|
66
|
+
export declare class AccountLinkConflictError extends FluoError {
|
|
67
|
+
readonly candidateAccountIds: string[];
|
|
68
|
+
constructor(candidateAccountIds: string[], message?: string);
|
|
69
|
+
}
|
|
70
|
+
/** Error raised when an account-linking attempt is rejected by policy. */
|
|
71
|
+
export declare class AccountLinkRejectedError extends FluoError {
|
|
72
|
+
constructor(message?: string, code?: string);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Conservative baseline policy that never auto-links ambiguous candidates.
|
|
76
|
+
*
|
|
77
|
+
* @returns An `AccountLinkPolicy` implementation that requires explicit user confirmation for non-explicit matches.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createConservativeAccountLinkPolicy(): AccountLinkPolicy;
|
|
80
|
+
/**
|
|
81
|
+
* Resolves account-linking flow using the provided policy and fallback rules.
|
|
82
|
+
*
|
|
83
|
+
* @param context Identity, candidate accounts, and optional explicit link-attempt confirmation input.
|
|
84
|
+
* @param policy Optional account-link policy implementation. When omitted, `options.fallback` controls behavior.
|
|
85
|
+
* @param options Runtime fallback behavior used only when `policy` is not configured.
|
|
86
|
+
* @returns A normalized framework decision: `linked`, `create-account`, or `skipped`.
|
|
87
|
+
* @throws {AccountLinkRejectedError} When policy returns `action: 'reject'`.
|
|
88
|
+
* @throws {AccountLinkConflictError} When policy returns `action: 'conflict'` and explicit confirmation is required.
|
|
89
|
+
*/
|
|
90
|
+
export declare function resolveAccountLinking(context: AccountLinkContext, policy?: AccountLinkPolicy, options?: AccountLinkingOptions): Promise<AccountLinkingResolution>;
|
|
91
|
+
//# sourceMappingURL=account-linking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-linking.d.ts","sourceRoot":"","sources":["../../src/account/account-linking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5D,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,eAAqD,CAAC;AAEzF,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,oEAAoE;AACpE,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,eAAe,GAAG,aAAa,GAAG,gBAAgB,GAAG,MAAM,CAAC;CACrE;AAED,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED,uEAAuE;AACvE,MAAM,MAAM,yBAAyB,GACjC;IACE,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GACD;IACE,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B,CAAC;AAEN,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,GAAG,YAAY,CAAC,yBAAyB,CAAC,CAAC;CAChF;AAED,8DAA8D;AAC9D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;CACtC;AAED,iEAAiE;AACjE,MAAM,MAAM,wBAAwB,GAChC;IACE,MAAM,EAAE,QAAQ,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,6EAA6E;AAC7E,qBAAa,wBAAyB,SAAQ,SAAS;IACrD,QAAQ,CAAC,mBAAmB,EAAE,MAAM,EAAE,CAAC;gBAGrC,mBAAmB,EAAE,MAAM,EAAE,EAC7B,OAAO,SAAoE;CAW9E;AAED,0EAA0E;AAC1E,qBAAa,wBAAyB,SAAQ,SAAS;gBACzC,OAAO,SAA0C,EAAE,IAAI,SAA0B;CAG9F;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,iBAAiB,CAsDvE;AAKD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,CAAC,EAAE,iBAAiB,EAC1B,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,wBAAwB,CAAC,CA0CnC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { FluoError } from '@fluojs/core';
|
|
2
|
+
|
|
3
|
+
/** DI token for registering an account-linking policy implementation. */
|
|
4
|
+
export const ACCOUNT_LINKING_POLICY = Symbol.for('fluo.passport.account-linking-policy');
|
|
5
|
+
|
|
6
|
+
/** Identity payload from an external authentication provider. */
|
|
7
|
+
|
|
8
|
+
/** Candidate account that may be linked to an external identity. */
|
|
9
|
+
|
|
10
|
+
/** Explicit user confirmation payload for a requested linking target. */
|
|
11
|
+
|
|
12
|
+
/** Input passed to account-link policy evaluation. */
|
|
13
|
+
|
|
14
|
+
/** Normalized decision contract returned by an `AccountLinkPolicy`. */
|
|
15
|
+
|
|
16
|
+
/** Policy contract for account-linking decisions. */
|
|
17
|
+
|
|
18
|
+
/** Optional runtime behavior when no policy is configured. */
|
|
19
|
+
|
|
20
|
+
/** Final framework-level resolution for account-linking flow. */
|
|
21
|
+
|
|
22
|
+
/** Error raised when linking requires user confirmation among candidates. */
|
|
23
|
+
export class AccountLinkConflictError extends FluoError {
|
|
24
|
+
candidateAccountIds;
|
|
25
|
+
constructor(candidateAccountIds, message = 'Multiple account candidates require explicit link confirmation.') {
|
|
26
|
+
super(message, {
|
|
27
|
+
code: 'ACCOUNT_LINK_CONFLICT',
|
|
28
|
+
meta: {
|
|
29
|
+
candidateAccountIds: [...candidateAccountIds]
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
this.candidateAccountIds = [...candidateAccountIds];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Error raised when an account-linking attempt is rejected by policy. */
|
|
37
|
+
export class AccountLinkRejectedError extends FluoError {
|
|
38
|
+
constructor(message = 'Account-linking attempt was rejected.', code = 'ACCOUNT_LINK_REJECTED') {
|
|
39
|
+
super(message, {
|
|
40
|
+
code
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Conservative baseline policy that never auto-links ambiguous candidates.
|
|
47
|
+
*
|
|
48
|
+
* @returns An `AccountLinkPolicy` implementation that requires explicit user confirmation for non-explicit matches.
|
|
49
|
+
*/
|
|
50
|
+
export function createConservativeAccountLinkPolicy() {
|
|
51
|
+
return {
|
|
52
|
+
evaluate(context) {
|
|
53
|
+
const existingLink = context.candidates.find(candidate => candidate.reason === 'existing-link');
|
|
54
|
+
if (existingLink) {
|
|
55
|
+
return {
|
|
56
|
+
action: 'link',
|
|
57
|
+
accountId: existingLink.accountId,
|
|
58
|
+
reason: 'Existing identity link found.'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const linkAttempt = context.linkAttempt;
|
|
62
|
+
if (linkAttempt) {
|
|
63
|
+
if (!linkAttempt.confirmedByUser) {
|
|
64
|
+
return {
|
|
65
|
+
action: 'reject',
|
|
66
|
+
code: 'ACCOUNT_LINK_CONFIRMATION_REQUIRED',
|
|
67
|
+
reason: 'Explicit link confirmation is required before linking identities.'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (context.candidates.some(candidate => candidate.accountId === linkAttempt.targetAccountId)) {
|
|
71
|
+
return {
|
|
72
|
+
action: 'link',
|
|
73
|
+
accountId: linkAttempt.targetAccountId,
|
|
74
|
+
reason: 'Identity linked after explicit user confirmation.'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
action: 'reject',
|
|
79
|
+
code: 'ACCOUNT_LINK_TARGET_NOT_FOUND',
|
|
80
|
+
reason: 'Requested account for linking is not a valid candidate.'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (context.candidates.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
action: 'create-account',
|
|
86
|
+
reason: 'No matching account candidate found for this external identity.'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
action: 'conflict',
|
|
91
|
+
candidateAccountIds: context.candidates.map(candidate => candidate.accountId),
|
|
92
|
+
reason: context.candidates.length === 1 ? 'A single account candidate matched. Explicit confirmation is required before linking.' : 'Multiple account candidates matched. Explicit confirmation is required before linking.'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const DEFAULT_SKIP_REASON = 'No account-linking policy was configured. The framework leaves identity linking to the application.';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolves account-linking flow using the provided policy and fallback rules.
|
|
101
|
+
*
|
|
102
|
+
* @param context Identity, candidate accounts, and optional explicit link-attempt confirmation input.
|
|
103
|
+
* @param policy Optional account-link policy implementation. When omitted, `options.fallback` controls behavior.
|
|
104
|
+
* @param options Runtime fallback behavior used only when `policy` is not configured.
|
|
105
|
+
* @returns A normalized framework decision: `linked`, `create-account`, or `skipped`.
|
|
106
|
+
* @throws {AccountLinkRejectedError} When policy returns `action: 'reject'`.
|
|
107
|
+
* @throws {AccountLinkConflictError} When policy returns `action: 'conflict'` and explicit confirmation is required.
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveAccountLinking(context, policy, options = {}) {
|
|
110
|
+
if (!policy) {
|
|
111
|
+
if (options.fallback === 'create-account') {
|
|
112
|
+
return {
|
|
113
|
+
reason: 'No policy configured. Falling back to account creation.',
|
|
114
|
+
status: 'create-account'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
reason: DEFAULT_SKIP_REASON,
|
|
119
|
+
status: 'skipped'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const decision = await policy.evaluate(context);
|
|
123
|
+
switch (decision.action) {
|
|
124
|
+
case 'link':
|
|
125
|
+
return {
|
|
126
|
+
accountId: decision.accountId,
|
|
127
|
+
reason: decision.reason,
|
|
128
|
+
status: 'linked'
|
|
129
|
+
};
|
|
130
|
+
case 'create-account':
|
|
131
|
+
return {
|
|
132
|
+
reason: decision.reason,
|
|
133
|
+
status: 'create-account'
|
|
134
|
+
};
|
|
135
|
+
case 'reject':
|
|
136
|
+
throw new AccountLinkRejectedError(decision.reason, decision.code);
|
|
137
|
+
case 'conflict':
|
|
138
|
+
throw new AccountLinkConflictError(decision.candidateAccountIds, decision.reason);
|
|
139
|
+
default:
|
|
140
|
+
{
|
|
141
|
+
const exhaustiveCheck = decision;
|
|
142
|
+
return exhaustiveCheck;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|