PHPackages                             baha2odeh/yii2-oauth - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. baha2odeh/yii2-oauth

ActiveYii2-extension[Authentication &amp; Authorization](/categories/authentication)

baha2odeh/yii2-oauth
====================

Lightweight OAuth2 authorization server extension for Yii2

0.0.5(3w ago)09MITPHPPHP &gt;=8.2CI passing

Since May 10Pushed 3w agoCompare

[ Source](https://github.com/Baha2Odeh/yii2-oauth)[ Packagist](https://packagist.org/packages/baha2odeh/yii2-oauth)[ RSS](/packages/baha2odeh-yii2-oauth/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (5)Dependencies (2)Versions (6)Used By (0)

yii2-oauth
==========

[](#yii2-oauth)

A lightweight, extensible OAuth2 authorization server extension for Yii2.

[![PHP](https://camo.githubusercontent.com/83dd395020c37276225039739320f6c8e7e99963ab21ee3d09282cb48dad2a60/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532422d626c7565)](https://camo.githubusercontent.com/83dd395020c37276225039739320f6c8e7e99963ab21ee3d09282cb48dad2a60/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532422d626c7565)[![Yii2](https://camo.githubusercontent.com/77f4c02e8ba7361e327ad37b9a360593f5f218d8d672c66428dc4289d9f84f0b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f596969322d322e302e35342d677265656e)](https://camo.githubusercontent.com/77f4c02e8ba7361e327ad37b9a360593f5f218d8d672c66428dc4289d9f84f0b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f596969322d322e302e35342d677265656e)[![CI](https://github.com/Baha2Odeh/yii2-oauth/actions/workflows/ci.yml/badge.svg)](https://github.com/Baha2Odeh/yii2-oauth/actions/workflows/ci.yml)[![License](https://camo.githubusercontent.com/b8cadaa967891081f8f165695470689986c028821dd8a040132f6e661795dc0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c7565)](https://camo.githubusercontent.com/b8cadaa967891081f8f165695470689986c028821dd8a040132f6e661795dc0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c7565)

---

Features
--------

[](#features)

- **Authorization Code** grant with optional **PKCE** (S256 and plain) and **automatic consent reuse**
- **Client Credentials** grant
- **Password** grant
- **Refresh Token** grant
- Token storage via **ActiveRecord** — no abstraction layers, pure Yii2 style
- Any model class is **swappable** from module config
- **Custom grant types** via a simple interface
- **BearerTokenAuth** filter — attach to any controller
- **Standalone actions** — attach token/authorize/userinfo to your own controllers
- **Console commands** for client and scope management
- Single database migration — SQLite, MySQL, PostgreSQL compatible

---

Requirements
------------

[](#requirements)

- PHP 8.1+
- Yii2 2.0.54+

---

Installation
------------

[](#installation)

```
composer require baha2odeh/yii2-oauth
```

Run the migration:

```
php yii migrate --migrationPath=@vendor/baha2odeh/yii2-oauth/migrations
```

---

Quick Start
-----------

[](#quick-start)

### 1. Configure the module

[](#1-configure-the-module)

Add the module to your application config (`config/web.php`):

```
'bootstrap' => ['oauth'],

'modules' => [
    'oauth' => [
        'class' => \baha2odeh\yii2oauth\OAuthModule::class,
        'userModelClass' => \app\models\User::class,  // your user AR class
    ],
],
```

The module self-registers these URL rules automatically — no `urlManager` changes needed:

MethodURLDescription`GET``/oauth/authorize`Authorization consent page`POST``/oauth/authorize/approve`User approves or denies consent`POST``/oauth/token`Token exchange endpoint`GET``/oauth/userinfo`Returns authenticated user claims### 2. Implement `UserEntityInterface` on your User model

[](#2-implement-userentityinterface-on-your-user-model)

```
use baha2odeh\yii2oauth\contracts\entities\UserEntityInterface;

class User extends \yii\db\ActiveRecord implements UserEntityInterface
{
    // Required by UserEntityInterface
    public function getIdentifier(): string
    {
        return (string) $this->id;
    }

    public function getClaims(): array
    {
        return [
            'email' => $this->email,
            'name' => $this->username,
        ];
    }

    // Required by the password grant
    public static function findByCredentials(string $username, string $password): ?static
    {
        $user = static::findOne(['username' => $username]);
        if ($user && \Yii::$app->security->validatePassword($password, $user->password_hash)) {
            return $user;
        }
        return null;
    }

    // Required by the /userinfo endpoint (standard Yii2 IdentityInterface)
    public static function findIdentity($id): ?static
    {
        return static::findOne($id);
    }
}
```

### 3. Create a client

[](#3-create-a-client)

```
php yii oauth/client/create --name="My App" --redirect-uris="https://myapp.com/callback"
```

Output:

```
Client created successfully!

Client ID:     a3f1c8d2e09b4561
Client Secret: 7f3a1b9c2d4e5f6a...
(Save the secret now — it cannot be retrieved again.)

```

### 4. Test the token endpoint

[](#4-test-the-token-endpoint)

```
# Client Credentials
curl -X POST https://your-app.com/oauth/token \
  -u "a3f1c8d2e09b4561:7f3a1b9c2d4e5f6a..." \
  -d "grant_type=client_credentials"
```

```
{
  "access_token": "e3b0c44298fc1c149a...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": ""
}
```

---

Grant Types
-----------

[](#grant-types)

### Authorization Code (with optional PKCE)

[](#authorization-code-with-optional-pkce)

The recommended grant for web and mobile apps.

#### Step 1 — Redirect the user

[](#step-1--redirect-the-user)

```
GET /oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.com/callback
  &scope=openid profile
  &state=RANDOM_STATE
  &code_challenge=PKCE_CHALLENGE        (optional, required for public clients with require_pkce=1)
  &code_challenge_method=S256           (S256 or plain, defaults to S256)

```

The user sees the consent page and approves or denies the request.

> **Automatic consent reuse:** If the user has previously approved this client and a valid (non-revoked, non-expired) access token still exists for the same user, client, and scopes, the consent screen is skipped entirely and the user is redirected back immediately with a new authorization code.

On approval, the server redirects back:

```
https://your-app.com/callback?code=AUTH_CODE&state=RANDOM_STATE

```

#### Step 2 — Exchange the code for tokens

[](#step-2--exchange-the-code-for-tokens)

```
curl -X POST https://your-app.com/oauth/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://your-app.com/callback" \
  -d "code_verifier=PKCE_VERIFIER"    # if PKCE was used
```

```
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "...",
  "scope": "openid profile"
}
```

#### PKCE Code Challenge Generation (PHP example)

[](#pkce-code-challenge-generation-php-example)

```
$verifier  = bin2hex(random_bytes(32));
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
// Store $verifier in session; send $challenge in the /authorize request
```

---

### Client Credentials

[](#client-credentials)

For machine-to-machine access. Confidential clients only.

```
curl -X POST https://your-app.com/oauth/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  -d "scope=api:read"
```

---

### Password

[](#password)

Directly exchange user credentials for tokens. Requires the `userModelClass` to implement `findByCredentials()`.

```
curl -X POST https://your-app.com/oauth/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d "grant_type=password" \
  -d "username=alice@example.com" \
  -d "password=secret" \
  -d "scope=profile"
```

---

### Refresh Token

[](#refresh-token)

Exchange a refresh token for a new access + refresh token pair. Old tokens are revoked immediately.

```
curl -X POST https://your-app.com/oauth/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "scope=profile"    # optional — must be a subset of the original scope
```

---

UserInfo Endpoint
-----------------

[](#userinfo-endpoint)

Returns the authenticated user's claims. Requires a valid Bearer token.

```
curl https://your-app.com/oauth/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```
{
  "sub": "42",
  "email": "alice@example.com",
  "name": "alice"
}
```

The claims come from `UserEntityInterface::getClaims()` on your user model.

---

Console Commands
----------------

[](#console-commands)

### Client Management

[](#client-management)

```
# Create a confidential client
php yii oauth/client/create \
  --name="My App" \
  --redirect-uris="https://app.com/cb" \
  --grant-types="authorization_code,refresh_token" \
  --scopes="openid profile email" \
  --confidential=1

# Create a public client (SPA/mobile)
php yii oauth/client/create \
  --name="My SPA" \
  --redirect-uris="https://app.com/cb" \
  --grant-types="authorization_code,refresh_token" \
  --confidential=0

# List all clients
php yii oauth/client/list

# Deactivate (reversible)
php yii oauth/client/deactivate CLIENT_ID

# Re-activate
php yii oauth/client/activate CLIENT_ID

# Permanently delete (cascades tokens and auth codes)
php yii oauth/client/delete CLIENT_ID

# Rotate secret (prints new secret once)
php yii oauth/client/reset-secret CLIENT_ID
```

### Scope Management

[](#scope-management)

```
# List registered scopes
php yii oauth/client/scopes

# Register a scope
php yii oauth/client/add-scope openid "OpenID Connect" --default=1
php yii oauth/client/add-scope profile "User profile"
php yii oauth/client/add-scope email "Email address"
```

### Maintenance

[](#maintenance)

```
# Purge expired tokens (run via cron)
php yii oauth/client/purge-tokens
```

Recommended cron entry:

```
0 * * * * /path/to/yii oauth/client/purge-tokens >> /dev/null 2>&1
```

---

Protecting Your Routes
----------------------

[](#protecting-your-routes)

### Using the BearerTokenAuth filter

[](#using-the-bearertokenauth-filter)

Attach the filter to any controller or action:

```
use baha2odeh\yii2oauth\filters\BearerTokenAuth;

class ApiController extends \yii\rest\Controller
{
    public function behaviors(): array
    {
        return array_merge(parent::behaviors(), [
            'oauth' => [
                'class' => BearerTokenAuth::class,
                'moduleId' => 'oauth',   // matches your module key, default 'oauth'
                'optional' => false,     // true = let unauthenticated requests through
            ],
        ]);
    }

    public function actionProfile(): array
    {
        // Access the validated token
        $token = \Yii::$app->params['oauth.token'];

        return [
            'user_id' => $token->getUserId(),
            'scopes' => $token->getScopes(),
        ];
    }
}
```

When a token is invalid or missing, the filter returns:

```
HTTP 401
WWW-Authenticate: Bearer realm="oauth"

{"error": "invalid_token", "error_description": "..."}
```

---

Attaching Actions to Your Own Controllers
-----------------------------------------

[](#attaching-actions-to-your-own-controllers)

Instead of the default controllers, you can attach the standalone actions to any existing controller:

```
use baha2odeh\yii2oauth\actions\AuthorizeAction;
use baha2odeh\yii2oauth\actions\TokenAction;
use baha2odeh\yii2oauth\actions\UserinfoAction;
use baha2odeh\yii2oauth\filters\BearerTokenAuth;

class OAuthController extends \yii\web\Controller
{
    public $enableCsrfValidation = false; // required for token endpoint

    public function behaviors(): array
    {
        return [
            'bearer' => [
                'class' => BearerTokenAuth::class,
                'only' => ['userinfo'],
            ],
        ];
    }

    public function actions(): array
    {
        return [
            'token' => TokenAction::class,
            'authorize' => [
                'class' => AuthorizeAction::class,
                'viewFile' => '@app/views/oauth/authorize', // override the consent view
                'loginUrl' => ['/site/login'],              // redirect here if user is not logged in
            ],
            'userinfo' => UserinfoAction::class,
        ];
    }
}
```

`AuthorizeAction` properties:

PropertyTypeDefaultDescription`viewFile``string``@baha2odeh/yii2oauth/views/authorize/index`Path to the consent view`loginUrl``string|array``['/site/login']`Where to redirect unauthenticated users`moduleId``string``oauth`The OAuth module ID in your app config`sessionKey``string``oauth_auth_request`Session key for persisting the auth request---

Full Configuration Reference
----------------------------

[](#full-configuration-reference)

```
'modules' => [
        'oauth' => [
            'class' => \baha2odeh\yii2oauth\OAuthModule::class,

            // ---- Required ----
            'userModelClass' => \app\models\User::class,

            // ---- Model overrides (swap any AR class) ----
            'clientModelClass' => \app\models\OAuthClient::class,
            'accessTokenModelClass' => \baha2odeh\yii2oauth\models\OAuthAccessToken::class,
            'refreshTokenModelClass' => \baha2odeh\yii2oauth\models\OAuthRefreshToken::class,
            'authCodeModelClass' => \baha2odeh\yii2oauth\models\OAuthAuthCode::class,
            'scopeModelClass' => \baha2odeh\yii2oauth\models\OAuthScope::class,

            // ---- Enabled grants ----
            'enabledGrants' => [
                'authorization_code',
                'client_credentials',
                'password',
                'refresh_token',
            ],

            // ---- Token TTLs (seconds) ----
            'accessTokenTtl' => 3600,       // 1 hour
            'refreshTokenTtl' => 2592000,    // 30 days
            'authCodeTtl' => 600,        // 10 minutes

            // ---- Token entropy ----
            'tokenBytes' => 32,              // 32 bytes → 64-char hex token

            // ---- PKCE ----
            // false: PKCE is optional globally; per-client require_pkce column overrides
            // true:  PKCE required for every client
            'requirePkce' => false,
            'allowedPkceMethods' => ['S256', 'plain'],

            // ---- Custom grants ----
            'customGrants' => [
                'device_code' => \app\oauth\DeviceCodeGrant::class,
            ],
        ],
],
```

---

Customization
-------------

[](#customization)

### Extending a Model

[](#extending-a-model)

```
namespace app\models;

use baha2odeh\yii2oauth\models\OAuthClient as BaseClient;

class OAuthClient extends BaseClient
{
    // Add extra scopes validation, multi-tenancy, etc.
    public static function validateCredentials(string $clientId, ?string $secret, string $grantType): ?static
    {
        $client = parent::validateCredentials($clientId, $secret, $grantType);
        // Additional checks (e.g. tenant isolation)
        return $client;
    }
}
```

Register the extended model:

```
'clientModelClass' => \app\models\OAuthClient::class,
```

### Writing a Custom Grant

[](#writing-a-custom-grant)

```
namespace app\oauth;

use baha2odeh\yii2oauth\grants\AbstractGrant;
use yii\web\Request;

class DeviceCodeGrant extends AbstractGrant
{
    public function getIdentifier(): string
    {
        return 'urn:ietf:params:oauth:grant-type:device_code';
    }

    public function respondToAccessTokenRequest(Request $request): array
    {
        $client = $this->validateClient($request);
        $deviceCode = $request->getBodyParam('device_code', '');

        // ... your device code validation logic ...

        $scopes = $this->validateScopes('', $client);
        $token = $this->issueAccessToken($client, null, $scopes, $this->accessTokenTtl);

        return $this->buildTokenResponse($token, null, $this->accessTokenTtl);
    }
}
```

Register it in config:

```
'customGrants' => [
    'urn:ietf:params:oauth:grant-type:device_code' => \app\oauth\DeviceCodeGrant::class,
],
```

### Overriding the Consent View

[](#overriding-the-consent-view)

Create a view at any path and set it on the action:

```
// In your controller
'authorize' => [
    'class'    => \baha2odeh\yii2oauth\actions\AuthorizeAction::class,
    'viewFile' => '@app/views/oauth/consent',
    'loginUrl' => ['/site/login'],
],
```

### Consent Reuse (Auto-Approve)

[](#consent-reuse-auto-approve)

By default, `AuthorizeAction` skips the consent screen when the user already has a valid access token for the same client and scopes. This means:

- **First authorization:** user sees the consent page and must approve.
- **Subsequent authorizations:** if a non-revoked, non-expired token exists covering the requested scopes, the user is redirected back immediately with a new auth code — no consent page shown.
- **Token expires or is revoked:** consent page is shown again.

This is the standard behavior used by most OAuth2 providers (Google, GitHub, etc.).

If you need to force consent every time (e.g. for sensitive scopes), you can override `AuthorizeAction` and bypass the auto-approve check:

```
use baha2odeh\yii2oauth\actions\AuthorizeAction;

class StrictAuthorizeAction extends AuthorizeAction
{
    // Override to always show consent — skip automatic approval
    // by not calling parent and re-implementing handleAuthorize without the token check
}
```

Available variables in the view:

VariableTypeDescription`$client``ClientEntityInterface`The requesting client`$scopes``array``identifier => description` pairs`$state``string|null`The state parameter from the request`$action``AuthorizeAction`The action instance (access `moduleId`)Minimal consent view example:

```

 wants access
