|
| 1 | +# OAuth2 Server |
| 2 | + |
| 3 | +A complete OAuth2 server implementation for Hyperf framework, based on [league/oauth2-server](https://oauth2.thephpleague.com/). |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- Full OAuth2 server implementation supporting: |
| 8 | + - Client Credentials Grant |
| 9 | + - Password Grant |
| 10 | + - Refresh Token Grant |
| 11 | + - Authorization Code Grant (with PKCE support) |
| 12 | +- Built-in commands for client management |
| 13 | +- Multiple storage backends (Eloquent ORM) |
| 14 | +- Customizable token lifetimes |
| 15 | +- Scope management |
| 16 | +- Event-driven architecture |
| 17 | + |
| 18 | +## Installation |
| 19 | + |
| 20 | +### 1. Install via Composer |
| 21 | + |
| 22 | +```bash |
| 23 | +composer require friendsofhyperf/oauth2-server |
| 24 | +``` |
| 25 | + |
| 26 | +### 2. Publish Configuration |
| 27 | + |
| 28 | +```bash |
| 29 | +php bin/hyperf.php vendor:publish friendsofhyperf/oauth2-server |
| 30 | +``` |
| 31 | + |
| 32 | +### 3. Generate Encryption Keys |
| 33 | + |
| 34 | +```bash |
| 35 | +# Generate private/public key pair |
| 36 | +php bin/hyperf.php oauth2:generate-keypair |
| 37 | +``` |
| 38 | + |
| 39 | +This will generate: |
| 40 | +- `storage/oauth2/private.key` - Private key for signing tokens |
| 41 | +- `storage/oauth2/public.key` - Public key for verifying tokens |
| 42 | + |
| 43 | +### 4. Run Migrations |
| 44 | + |
| 45 | +```bash |
| 46 | +php bin/hyperf.php migrate |
| 47 | +``` |
| 48 | + |
| 49 | +## Configuration |
| 50 | + |
| 51 | +Configure your OAuth2 server in `config/autoload/oauth2-server.php`: |
| 52 | + |
| 53 | +```php |
| 54 | +<?php |
| 55 | + |
| 56 | +return [ |
| 57 | + 'authorization_server' => [ |
| 58 | + 'private_key' => env('OAUTH2_PRIVATE_KEY', 'storage/oauth2/private.key'), |
| 59 | + 'private_key_passphrase' => env('OAUTH2_PRIVATE_KEY_PASSPHRASE'), |
| 60 | + 'encryption_key' => env('OAUTH2_ENCRYPTION_KEY'), |
| 61 | + 'encryption_key_type' => EncryptionKeyType::from(env('OAUTH2_ENCRYPTION_KEY_TYPE', 'plain')), |
| 62 | + 'response_type' => BearerTokenResponse::class, |
| 63 | + 'revoke_refresh_tokens' => true, |
| 64 | + 'access_token_ttl' => new DateInterval('PT1H'), |
| 65 | + 'auth_code_ttl' => new DateInterval('PT10M'), |
| 66 | + 'refresh_token_ttl' => new DateInterval('P1M'), |
| 67 | + 'enable_client_credentials_grant' => true, |
| 68 | + 'enable_password_grant' => true, |
| 69 | + 'enable_refresh_token_grant' => true, |
| 70 | + 'enable_auth_code_grant' => true, |
| 71 | + 'enable_implicit_grant' => false, |
| 72 | + 'require_code_challenge_for_public_clients' => true, |
| 73 | + 'persist_access_tokens' => true, |
| 74 | + ], |
| 75 | + 'resource_server' => [ |
| 76 | + 'public_key' => env('OAUTH2_PUBLIC_KEY', 'storage/oauth2/public.key'), |
| 77 | + 'jwt_leeway' => null, |
| 78 | + ], |
| 79 | + 'scopes' => [ |
| 80 | + 'available' => ['read', 'write', 'admin'], |
| 81 | + 'default' => ['read'], |
| 82 | + ], |
| 83 | +]; |
| 84 | +``` |
| 85 | + |
| 86 | +## Environment Variables |
| 87 | + |
| 88 | +Set these environment variables in your `.env` file: |
| 89 | + |
| 90 | +```bash |
| 91 | +# OAuth2 Keys |
| 92 | +OAUTH2_PRIVATE_KEY=storage/oauth2/private.key |
| 93 | +OAUTH2_PUBLIC_KEY=storage/oauth2/public.key |
| 94 | +OAUTH2_PRIVATE_KEY_PASSPHRASE= |
| 95 | +OAUTH2_ENCRYPTION_KEY=your-encryption-key-here |
| 96 | + |
| 97 | +# Optional |
| 98 | +OAUTH2_ENCRYPTION_KEY_TYPE=plain |
| 99 | +``` |
| 100 | + |
| 101 | +## Available Commands |
| 102 | + |
| 103 | +| Command | Description | |
| 104 | +|---------|-------------| |
| 105 | +| `oauth2:clear-expired-tokens` | Remove expired access/refresh tokens | |
| 106 | +| `oauth2:create-client` | Create a new OAuth2 client | |
| 107 | +| `oauth2:delete-client` | Delete an OAuth2 client | |
| 108 | +| `oauth2:generate-keypair` | Generate private/public key pair | |
| 109 | +| `oauth2:list-clients` | List all OAuth2 clients | |
| 110 | +| `oauth2:update-client` | Update an OAuth2 client | |
| 111 | + |
| 112 | +### Creating Clients |
| 113 | + |
| 114 | +Create a client for Authorization Code Grant: |
| 115 | + |
| 116 | +```bash |
| 117 | +php bin/hyperf.php oauth2:create-client \ |
| 118 | + --name="My Web App" \ |
| 119 | + --redirect-uri="https://myapp.com/callback" \ |
| 120 | + --grant-type="authorization_code" \ |
| 121 | + --grant-type="refresh_token" |
| 122 | +``` |
| 123 | + |
| 124 | +Create a client for Password Grant: |
| 125 | + |
| 126 | +```bash |
| 127 | +php bin/hyperf.php oauth2:create-client \ |
| 128 | + --name="My Mobile App" \ |
| 129 | + --grant-type="password" \ |
| 130 | + --grant-type="refresh_token" |
| 131 | +``` |
| 132 | + |
| 133 | +Create a client for Client Credentials Grant: |
| 134 | + |
| 135 | +```bash |
| 136 | +php bin/hyperf.php oauth2:create-client \ |
| 137 | + --name="My API Service" \ |
| 138 | + --grant-type="client_credentials" |
| 139 | +``` |
| 140 | + |
| 141 | +## API Endpoints |
| 142 | + |
| 143 | +### Authorization Endpoint |
| 144 | + |
| 145 | +`GET /oauth/authorize` |
| 146 | + |
| 147 | +Used for Authorization Code Grant flow. Parameters: |
| 148 | +- `response_type`: Must be `code` |
| 149 | +- `client_id`: Your client ID |
| 150 | +- `redirect_uri`: Must match registered redirect URI |
| 151 | +- `scope`: Space-separated list of scopes |
| 152 | +- `state`: CSRF protection token |
| 153 | +- `code_challenge`: PKCE code challenge |
| 154 | +- `code_challenge_method`: PKCE method (usually `S256`) |
| 155 | + |
| 156 | +### Token Endpoint |
| 157 | + |
| 158 | +`POST /oauth/token` |
| 159 | + |
| 160 | +Used to exchange authorization code for access token or use other grant types. |
| 161 | + |
| 162 | +### Protected Resources |
| 163 | + |
| 164 | +Use `ResourceServerMiddleware` to protect your routes: |
| 165 | + |
| 166 | +```php |
| 167 | +use FriendsOfHyperf/oauth2/server/Middleware//ResourceServerMiddleware; |
| 168 | + |
| 169 | +Router::addGroup('/api', function () { |
| 170 | + Router::get('user', [UserController::class, 'index']); |
| 171 | + Router::post('posts', [PostController::class, 'store']); |
| 172 | +})->add(ResourceServerMiddleware::class); |
| 173 | +``` |
| 174 | + |
| 175 | +## Grant Types |
| 176 | + |
| 177 | +### 1. Client Credentials Grant |
| 178 | + |
| 179 | +For server-to-server authentication: |
| 180 | + |
| 181 | +```bash |
| 182 | +curl -X POST http://your-server/oauth/token \ |
| 183 | + -H "Content-Type: application/json" \ |
| 184 | + -d '{ |
| 185 | + "grant_type": "client_credentials", |
| 186 | + "client_id": "your-client-id", |
| 187 | + "client_secret": "your-client-secret", |
| 188 | + "scope": "read write" |
| 189 | + }' |
| 190 | +``` |
| 191 | + |
| 192 | +### 2. Password Grant |
| 193 | + |
| 194 | +For trusted applications (mobile apps, SPAs): |
| 195 | + |
| 196 | +```bash |
| 197 | +curl -X POST http://your-server/oauth/token \ |
| 198 | + -H "Content-Type: application/json" \ |
| 199 | + -d '{ |
| 200 | + "grant_type": "password", |
| 201 | + "client_id": "your-client-id", |
| 202 | + "client_secret": "your-client-secret", |
| 203 | + "username": "user@example.com", |
| 204 | + "password": "password", |
| 205 | + "scope": "read write" |
| 206 | + }' |
| 207 | +``` |
| 208 | + |
| 209 | +### 3. Authorization Code Grant |
| 210 | + |
| 211 | +For web applications with user interaction: |
| 212 | + |
| 213 | +**Step 1: Redirect user to authorization endpoint** |
| 214 | + |
| 215 | +``` |
| 216 | +https://your-server/oauth/authorize?response_type=code&client_id=your-client-id&redirect_uri=https://myapp.com/callback&scope=read&state=random-state&code_challenge=challenge&code_challenge_method=S256 |
| 217 | +``` |
| 218 | + |
| 219 | +**Step 2: Exchange code for token** |
| 220 | + |
| 221 | +```bash |
| 222 | +curl -X POST http://your-server/oauth/token \ |
| 223 | + -H "Content-Type: application/json" \ |
| 224 | + -d '{ |
| 225 | + "grant_type": "authorization_code", |
| 226 | + "client_id": "your-client-id", |
| 227 | + "client_secret": "your-client-secret", |
| 228 | + "redirect_uri": "https://myapp.com/callback", |
| 229 | + "code_verifier": "verifier", |
| 230 | + "code": "authorization-code-from-redirect" |
| 231 | + }' |
| 232 | +``` |
| 233 | + |
| 234 | +### 4. Refresh Token Grant |
| 235 | + |
| 236 | +To get new access tokens: |
| 237 | + |
| 238 | +```bash |
| 239 | +curl -X POST http://your-server/oauth/token \ |
| 240 | + -H "Content-Type: application/json" \ |
| 241 | + -d '{ |
| 242 | + "grant_type": "refresh_token", |
| 243 | + "client_id": "your-client-id", |
| 244 | + "client_secret": "your-client-secret", |
| 245 | + "refresh_token": "your-refresh-token", |
| 246 | + "scope": "read write" |
| 247 | + }' |
| 248 | +``` |
| 249 | + |
| 250 | +## Making Authenticated Requests |
| 251 | + |
| 252 | +Include the access token in the Authorization header: |
| 253 | + |
| 254 | +```bash |
| 255 | +curl -X GET http://your-server/api/user \ |
| 256 | + -H "Authorization: Bearer your-access-token" |
| 257 | +``` |
| 258 | + |
| 259 | +## Events |
| 260 | + |
| 261 | +The component dispatches several events you can listen to: |
| 262 | + |
| 263 | +- `AuthorizationRequestResolveEvent`: When authorization request needs user approval |
| 264 | +- `UserResolveEvent`: When resolving user for password grant |
| 265 | +- `ScopeResolveEvent`: When resolving scopes |
| 266 | +- `TokenRequestResolveEvent`: When processing token requests |
| 267 | + |
| 268 | +### Example Event Listener |
| 269 | + |
| 270 | +```php |
| 271 | +<?php |
| 272 | + |
| 273 | +namespace App\Listener; |
| 274 | + |
| 275 | +use FriendsOfHyperf\Oauth2\Server\Event\UserResolveEvent; |
| 276 | +use Hyperf\Event\Annotation\Listener; |
| 277 | + |
| 278 | +#[Listener] |
| 279 | +class UserResolveListener |
| 280 | +{ |
| 281 | + public function listen(): array |
| 282 | + { |
| 283 | + return [ |
| 284 | + UserResolveEvent::class, |
| 285 | + ]; |
| 286 | + } |
| 287 | + |
| 288 | + public function process(object $event): void |
| 289 | + { |
| 290 | + // Validate user credentials and return user ID |
| 291 | + if ($event->getUsername() === 'admin' && $event->getPassword() === 'secret') { |
| 292 | + $event->setUserId('1'); |
| 293 | + } |
| 294 | + } |
| 295 | +} |
| 296 | +``` |
| 297 | + |
| 298 | +## Database Tables |
| 299 | + |
| 300 | +The package creates the following tables: |
| 301 | + |
| 302 | +- `oauth_clients`: OAuth2 clients |
| 303 | +- `oauth_access_tokens`: Access tokens |
| 304 | +- `oauth_refresh_tokens`: Refresh tokens |
| 305 | +- `oauth_auth_codes`: Authorization codes |
| 306 | +- `oauth_personal_access_clients`: Personal access clients |
| 307 | + |
| 308 | +## Customization |
| 309 | + |
| 310 | +### Custom User Provider |
| 311 | + |
| 312 | +Implement your own user resolution logic by listening to `UserResolveEvent`. |
| 313 | + |
| 314 | +### Custom Scope Management |
| 315 | + |
| 316 | +Listen to `ScopeResolveEvent` to implement custom scope logic. |
| 317 | + |
| 318 | +### Custom Token Storage |
| 319 | + |
| 320 | +Extend the repository classes to implement custom storage backends. |
| 321 | + |
| 322 | +## Security Best Practices |
| 323 | + |
| 324 | +1. Always use HTTPS in production |
| 325 | +2. Store private keys securely with proper file permissions |
| 326 | +3. Use strong encryption keys |
| 327 | +4. Implement proper CSRF protection for authorization flows |
| 328 | +5. Validate redirect URIs strictly |
| 329 | +6. Use short-lived access tokens and refresh tokens |
| 330 | +7. Implement rate limiting on token endpoints |
| 331 | +8. Log and monitor token usage |
| 332 | + |
| 333 | +## Testing |
| 334 | + |
| 335 | +During development, you can test the OAuth2 flow with the built-in commands: |
| 336 | + |
| 337 | +```bash |
| 338 | +# Create a test client |
| 339 | +php bin/hyperf.php oauth2:create-client \ |
| 340 | + --name="Test Client" \ |
| 341 | + --redirect-uri="http://localhost:3000/callback" \ |
| 342 | + --grant-type="authorization_code" \ |
| 343 | + --grant-type="password" \ |
| 344 | + --grant-type="refresh_token" |
| 345 | + |
| 346 | +# List all clients |
| 347 | +php bin/hyperf.php oauth2:list-clients |
| 348 | + |
| 349 | +# Clear expired tokens |
| 350 | +php bin/hyperf.php oauth2:clear-expired-tokens |
| 351 | +``` |
| 352 | + |
| 353 | +## Error Handling |
| 354 | + |
| 355 | +Common error responses: |
| 356 | + |
| 357 | +- `invalid_client`: Client authentication failed |
| 358 | +- `invalid_grant`: Invalid authorization grant |
| 359 | +- `invalid_request`: Missing required parameters |
| 360 | +- `invalid_scope`: Requested scope is invalid |
| 361 | +- `unsupported_grant_type`: Grant type not supported |
| 362 | +- `server_error`: Internal server error |
| 363 | + |
| 364 | +## License |
| 365 | + |
| 366 | +MIT |
0 commit comments