OAuth 2.0 Authentication Implementation for Web Application

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing OAuth2 Authentication for Web Applications

OAuth2 is a delegated authorization protocol. A user allows a third-party application access to their data on another service without sharing their password. Google, GitHub, and Facebook act as Identity Providers (IdPs), and your application is the Client. OAuth2 ≠ authentication—it's about authorizing access to resources, but authentication is built on top of it via OpenID Connect (OIDC).

Authorization Code Flow (Primary)

Sequence for web applications:

  1. Client → IdP: GET /oauth/authorize?response_type=code&client_id=...&redirect_uri=...&scope=openid email&state=random
  2. User logs in at IdP and grants permission
  3. IdP → Client: GET /callback?code=AUTH_CODE&state=random
  4. Client → IdP backend: POST /oauth/token with code → receives access_token, id_token, refresh_token
  5. Client → IdP: GET /userinfo with access_token → user profile

State — CSRF protection: generated before redirect, verified on return.

PKCE (Proof Key for Code Exchange) — mandatory for SPAs and mobile apps where there is no server-side client_secret storage:

// Generate code_verifier and code_challenge
const verifier = crypto.randomUUID().replace(/-/g, '') + crypto.randomUUID().replace(/-/g, '');
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

// Store verifier in sessionStorage
sessionStorage.setItem('pkce_verifier', verifier);

// Add to authorize URL
const url = `${authUrl}?...&code_challenge=${challenge}&code_challenge_method=S256`;

OAuth2 Server Implementation on Laravel (Laravel Passport)

If your application is itself an OAuth2 server (issuing tokens for API clients or a mobile app):

composer require laravel/passport
php artisan passport:install
// User model
use HasApiTokens;

// AuthServiceProvider
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));

// Create client for SPA (with PKCE)
php artisan passport:client --public
// Create client for server-to-server
php artisan passport:client --client

Scopes for granular access control:

// AuthServiceProvider
Passport::tokensCan([
    'read:profile'  => 'Read profile',
    'write:profile' => 'Edit profile',
    'read:orders'   => 'Read orders',
    'write:orders'  => 'Create orders',
]);

// Protect routes by scope
Route::middleware(['auth:api', 'scope:read:orders'])->get('/orders', ...);

Client Credentials Flow (Machine-to-Machine)

For server-to-server without user involvement:

// Get token (microservice A requests token from OAuth server)
$response = Http::post('https://auth.example.com/oauth/token', [
    'grant_type'    => 'client_credentials',
    'client_id'     => config('services.microservice_a.client_id'),
    'client_secret' => config('services.microservice_a.client_secret'),
    'scope'         => 'read:inventory write:orders',
]);

$accessToken = $response->json('access_token');
$expiresIn   = $response->json('expires_in'); // seconds

// Cache token until expiration
Cache::put('microservice_token', $accessToken, now()->addSeconds($expiresIn - 60));

Introspection and Token Validation

Resource server validates the token:

// Option 1: Local JWT verification (fastest)
$token = JWT::decode($accessToken, $publicKey, ['RS256']);

// Option 2: Token introspection endpoint (RFC 7662)
$response = Http::withBasicAuth($resourceServerId, $resourceServerSecret)
    ->post('https://auth.example.com/oauth/introspect', [
        'token' => $accessToken,
    ]);

$data = $response->json();
if (!$data['active']) {
    return response()->json(['error' => 'Token inactive'], 401);
}

OIDC — Authentication on Top of OAuth2

OpenID Connect adds an id_token (JWT with user data) to OAuth2:

// Decode id_token (after verifying signature)
const [header, payload, signature] = idToken.split('.');
const claims = JSON.parse(atob(payload));

// claims contains:
// sub: "user_123"          — unique user ID at IdP
// email: "[email protected]"
// name: "John Doe"
// picture: "https://..."
// iss: "https://accounts.google.com" — who issued the token
// aud: "your_client_id"   — who the token is for
// exp: 1735689600          — expiration

// CRITICALLY IMPORTANT: verify signature before using claims!
import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://accounts.google.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(idToken, JWKS, {
  issuer: 'https://accounts.google.com',
  audience: process.env.GOOGLE_CLIENT_ID,
});

Refresh Token Rotation

// On access_token expiration—exchange refresh_token for a new pair
$response = Http::post('/oauth/token', [
    'grant_type'    => 'refresh_token',
    'refresh_token' => $user->refresh_token,
    'client_id'     => config('passport.client_id'),
    'client_secret' => config('passport.client_secret'),
    'scope'         => '',
]);

$user->update([
    'access_token'  => $response->json('access_token'),
    'refresh_token' => $response->json('refresh_token'), // rotate!
    'token_expires_at' => now()->addSeconds($response->json('expires_in')),
]);

Refresh Token Rotation: every time a refresh_token is used, it is invalidated and a new one is issued. If someone uses a stolen refresh_token, the old one is already invalid and the attack is detected.

Providers: Social Login

For "Log in with Google/GitHub"—use Socialite (Laravel) or Passport.js/next-auth:

// Socialite
Route::get('/auth/google/redirect', fn() => Socialite::driver('google')->redirect());

Route::get('/auth/google/callback', function () {
    $googleUser = Socialite::driver('google')->user();

    $user = User::updateOrCreate(
        ['email' => $googleUser->getEmail()],
        [
            'name'              => $googleUser->getName(),
            'avatar'            => $googleUser->getAvatar(),
            'google_id'         => $googleUser->getId(),
            'email_verified_at' => now(),
        ]
    );

    Auth::login($user, remember: true);
    return redirect('/dashboard');
});

Timeline

OAuth2 server with Passport, authorization code + client credentials, PKCE for SPA, scopes, introspection: 1–2 weeks. Social Login (Google, GitHub, VK) via Socialite: 2–3 days. OIDC with JWT verification and refresh rotation: 3–5 days.