Single Sign-On (SSO) 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

SSO (Single Sign-On) Implementation for Web Application

SSO is not just "login with Google". It's an architectural decision affecting session model, token security, authentication lifecycle, and corporate infrastructure integration. Poorly designed SSO becomes a single point of failure — literally: if Identity Provider is unavailable, users cannot log into any application.

Protocols and Their Applicability

Two current standards: SAML 2.0 and OpenID Connect (OIDC). SAML is used in corporate environments (Azure AD, Okta, ADFS) — XML-based, cumbersome, but universally supported. OIDC is built on OAuth 2.0, JSON-based, native to web and mobile.

For new projects, choice is almost always OIDC. Exception — integration with legacy enterprise IdP supporting only SAML. In this case, install a broker (Keycloak, Dex) that accepts SAML and provides OIDC downstream.

Basic Authorization Code flow with PKCE:

Browser → /authorize?response_type=code&code_challenge=... → IdP
IdP → callback?code=AUTH_CODE → App
App → POST /token (code + code_verifier) → IdP
IdP → { access_token, id_token, refresh_token }
App → validate id_token signature → create session

PKCE is mandatory for public clients (SPA, mobile) — protects against authorization code interception.

Architecture with Multiple Applications

In classic SSO, central session is stored at IdP, applications maintain their own short-lived sessions. Single Logout (SLO) mechanism requires special attention: on logout from one app, IdP must notify others via backchannel (server-to-server) or front-channel (redirects through browser).

Multiple Service Provider scheme:

┌──────────┐    ┌──────────┐    ┌──────────┐
│  App A   │    │  App B   │    │  App C   │
└────┬─────┘    └────┬─────┘    └────┬─────┘
     │               │               │
     └───────────────┴───────────────┘
                     │
              ┌──────▼──────┐
              │  IdP        │
              │  (Keycloak) │
              └─────────────┘

Each application is a client in IdP terminology with its own configuration: allowed redirect URIs, scopes, token lifetime.

Token Validation

id_token is a JWT. Validation is mandatory on each request if token is passed as credentials. Minimum checks:

from jwt import PyJWT, algorithms
import requests

def validate_id_token(token: str, client_id: str, issuer: str) -> dict:
    # Get IdP public keys
    jwks_uri = f"{issuer}/.well-known/openid-configuration"
    config = requests.get(jwks_uri).json()
    jwks = requests.get(config["jwks_uri"]).json()

    header = PyJWT.decode_header(token)
    key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
    public_key = algorithms.RSAAlgorithm.from_jwk(key)

    claims = PyJWT.decode(
        token,
        public_key,
        algorithms=["RS256"],
        audience=client_id,
        issuer=issuer,
        options={"verify_exp": True}
    )
    return claims

IdP keys should be cached with TTL, not requested on every call. Simultaneously, need ability to invalidate cache on key rotation (keys_changed event or just short TTL 1–6 hours).

Laravel Application Integration

Laravel has socialite package for simple providers (Google, GitHub) and league/oauth2-client for custom OIDC. For corporate SSO often use aacotroneo/laravel-saml2 or direct integration via firebase/php-jwt.

Example OIDC callback:

// routes/web.php
Route::get('/auth/callback', [SsoController::class, 'callback']);

// SsoController.php
public function callback(Request $request): RedirectResponse
{
    $code = $request->input('code');
    $state = $request->input('state');

    // Verify state against CSRF
    if ($state !== session('oauth_state')) {
        abort(400, 'Invalid state');
    }

    $tokens = $this->oidcClient->exchangeCode($code);
    $claims = $this->oidcClient->validateIdToken($tokens['id_token']);

    $user = User::updateOrCreate(
        ['sub' => $claims['sub']],
        [
            'email'      => $claims['email'],
            'name'       => $claims['name'],
            'provider'   => 'corporate_sso',
            'last_login' => now(),
        ]
    );

    Auth::login($user, remember: true);

    return redirect()->intended('/dashboard');
}

Field sub (subject) — stable user identifier from IdP. Email can change, sub cannot. Link accounts by sub, not email.

Single Logout

SLO is more complex than it seems. Backchannel logout: IdP sends POST to logout endpoint of each app with logout_token (JWT with sid). App finds session by sid and destroys it.

Route::post('/auth/backchannel-logout', function (Request $request) {
    $logoutToken = $request->input('logout_token');
    $claims = validateLogoutToken($logoutToken); // similar to id_token

    $sessionId = $claims['sid'];

    // Delete all sessions with given SSO session ID
    DB::table('sessions')
        ->where('sso_session_id', $sessionId)
        ->delete();

    return response()->noContent();
})->middleware('throttle:60,1');

Front-channel logout works via iframe: IdP opens logout URL of each app in hidden frames. Reliability lower — depends on browser policy (ITP in Safari blocks third-party cookies in iframe).

Error Handling and Edge Cases

  • IdP unavailable: need fallback — local login form with password or graceful degradation with "SSO temporarily unavailable" message
  • IdP session expiration during active work: token refresh via refresh_token should happen transparently, without user interruption
  • User email change: if IdP updated email, app should respond correctly — don't create duplicate account
  • Multi-tenancy: different clients may have different IdPs. Determine IdP by email domain or tenant_id in URL (app.example.com/{tenant}/login)

Implementation Timeline

  • Integration with one OIDC provider (Google Workspace, Azure AD) — 3–5 days
  • Own IdP on Keycloak with multiple apps — 2–3 weeks
  • SAML + OIDC broker with multi-tenancy — from 4 weeks

Most time goes not to code but to IdP configuration, testing authentication edge cases, and SLO setup. Tokens that "seem to work" may contain invalid claims or fail validation in production due to server clock drift (claim iat/exp).