SurillyaID OpenID Connect (OIDC)

Documentation on how you can easily integrate SurillyaID sign-in into your applications!

Use OpenID Connect

OpenID Connect is the recommended protocol for SurillyaID integration. It provides:

  • 12 scopes for user data (see below)
  • ID Tokens (JWT) with built-in signature verification
  • Standard claims following OIDC specifications
  • Better library support across all major languages and frameworks

OAuth2 is also supported but has only basic authentication without the rich scope ecosystem.

Quick Start

Prerequisites

  1. Create a developer account at developer.surillya.com
  2. Create an application and get your Client ID and Secret
  3. Configure your redirect URI
  4. Always include openid scope to enable OIDC features

OpenID Connect Discovery

SurillyaID supports easy OIDC Discovery:

https://surillya.com/id/.well-known/openid-configuration

Most OIDC libraries can auto-configure using this endpoint. No manual endpoint configuration needed!

API Endpoints

Authorization Endpoint

https://surillya.com/id/authorize.php

Initiates the OAuth flow and prompts users to authorize your application

Token Endpoint

https://surillya.com/id/token.php

Exchanges authorization codes for access tokens

UserInfo Endpoint

https://surillya.com/id/userinfo.php

Returns user information based on granted scopes

JWKS Endpoint

https://surillya.com/id/jwks.php

Public keys for verifying JWT signatures

Token Revocation

https://surillya.com/id/revoke.php

Revoke access tokens (RFC 7009)

List of all available claims

Here's a complete list of all 30 claims available through SurillyaID OIDC. Claims are returned based on the scopes you request:

User Identity & Profile Claims

Claim Type Description Required Scope
sub string Subject identifier (user ID) openid
id string User ID (alias for sub) openid
name string Full display name profile
username string Unique username profile
preferred_username string Preferred username (same as username) profile
email string Email address email
email_verified boolean Whether email is verified email
picture URL Profile picture URL profile
banner URL Profile banner image URL profile
bio string User biography/about text profile
verified boolean Whether user has verified badge profile

Extended Claims

Claim Type Description Required Scope
location string Geographic location location
age integer User's age demographics
gender string User's gender demographics
profession string User's profession/occupation professional
education string Education level/institution professional
minecraft_username string Minecraft username minecraft
minecraft_uuid string Minecraft player UUID minecraft
spotify_track string Currently playing Spotify track spotify
social_links JSON array Array of social media profile links social
material_colors JSON object Material You dynamic color palette theme
theme_color1 string Primary theme color (hex) theme
theme_color2 string Secondary theme color (hex) theme
citizenship string MDU citizenship status citizenship
is_supporter boolean Whether user is a supporter supporter
supporter_since datetime When user became a supporter supporter

JWT Standard Claims (ID Token only)

Claim Type Description
iss string Issuer (https://surillya.com/id)
aud string Audience (your client_id)
exp timestamp Expiration time
iat timestamp Issued at time

OIDC Authorization Flow (with PKCE)

This is the recommended flow for all applications. PKCE provides additional security for both web and mobile apps.

Step 1: Generate PKCE Challenge

// Generate code verifier (random string)
code_verifier = base64url(random(32 bytes))

// Generate code challenge (SHA-256 hash)
code_challenge = base64url(sha256(code_verifier))

Step 2: Redirect to Authorization Endpoint

Include the openid scope plus any additional scopes you need:

https://surillya.com/id/authorize.php?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=YOUR_REDIRECT_URI&
  response_type=code&
  scope=openid profile email&
  state=RANDOM_STATE&
  code_challenge=CODE_CHALLENGE&
  code_challenge_method=S256&
  nonce=RANDOM_NONCE

Step 3: Exchange Code for Tokens

Response includes both Access Token and ID Token (JWT):

POST https://surillya.com/id/token.php
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=YOUR_REDIRECT_URI&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
code_verifier=CODE_VERIFIER

// Response:
{
  "access_token": "...",
  "id_token": "...",        // JWT with user claims
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email"
}

Step 4: Get User Information

Two options - decode ID Token (faster) or call UserInfo endpoint:

// Option 1: Decode and verify ID Token JWT (recommended)
// Contains claims based on requested scopes
// Verify signature using JWKS endpoint

// Option 2: Call UserInfo endpoint
GET https://surillya.com/id/userinfo.php
Authorization: Bearer ACCESS_TOKEN

OIDC Scopes (12 Available)

SurillyaID provides 12 comprehensive scopes for accessing different types of user data. Always include openid to enable OIDC.

Core OIDC Scopes (Standard)

Scope Description Claims Returned
openid Required for OIDC - User identifier sub, id
profile Basic profile information name, username, picture, banner, bio, verified
email Email address and verification status email, email_verified

Extended Scopes (SurillyaID-Specific)

Scope Description Claims Returned
location User's location location
demographics Age and gender information age, gender
professional Career and education data profession, education
minecraft Minecraft profile (username & UUID) minecraft_username, minecraft_uuid
spotify Currently playing Spotify track spotify_track
social Social media profile links social_links (JSON array)
theme Material You theme colors & legacy colors material_colors (object), theme_color1, theme_color2
citizenship Surillya citizenship status citizenship
supporter Supporter/donor status is_supporter, supporter_since

Scope Best Practices

  • Always request openid as the first scope to enable OIDC
  • Request only what you need - Users see every scope you request
  • Combine scopes: openid profile email demographics
  • Common combinations:
    • Basic login: openid profile email
    • Gaming platform: openid profile email minecraft
    • Social app: openid profile email social demographics
    • Music app: openid profile spotify

Using OIDC Libraries (Recommended)

For production applications, we strongly recommend using established OIDC libraries. These handle critical security features (PKCE, State validation, JWT signature verification) automatically.

Node.js (Express + Passport)

This example uses passport-openidconnect for authentication and connect-redis for secure session storage.

1. Installation:

npm install express express-session passport openid-client dotenv connect-redis redis

Requirement: You must have a Redis server running locally or accessible via network.

2. Setup Environment Variables (.env):

Create a .env file in your project root. Do not commit this file to version control.

# Get these from the Surillya Developer Dashboard
SURILLYA_CLIENT_ID=your_client_id_here
SURILLYA_CLIENT_SECRET=your_client_secret_here

# Generate a strong random string (e.g. `openssl rand -hex 32`)
SESSION_SECRET=long_random_string_here

# Production Settings
NODE_ENV=production

3. Application Code (server.js):

require('dotenv').config();
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const { Issuer, Strategy } = require('openid-client');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const app = express();

// --- 1. Database & Session Setup ---
// Initialize Redis client for persistent session storage
const redisClient = createClient();
redisClient.connect().catch(console.error);

// Initialize session store
const sessionStore = new RedisStore({
    client: redisClient,
    prefix: "surillya_sess:",
});

// Configure session middleware
app.use(session({
    store: sessionStore,
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: process.env.NODE_ENV === 'production', // true in production (requires HTTPS)
        httpOnly: true, // Prevents client-side JS from reading the cookie
        maxAge: 24 * 60 * 60 * 1000 // 24 hours
    }
}));

// --- 2. Authentication Setup ---
app.use(passport.initialize());
app.use(passport.session());

// Serialization: How to store user in session
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));

async function main() {
    // Discover OIDC endpoints automatically
    const issuer = await Issuer.discover('https://surillya.com/id/.well-known/openid-configuration');
    
    const client = new issuer.Client({
        client_id: process.env.SURILLYA_CLIENT_ID,
        client_secret: process.env.SURILLYA_CLIENT_SECRET,
        redirect_uris: ['http://localhost:3000/callback'],
        response_types: ['code']
    });

    passport.use('oidc', new Strategy({ 
        client, 
        params: {
            scope: 'openid profile email',
            code_challenge_method: 'S256'
        },
        usePKCE: true
    }, (tokenSet, userinfo, done) => {
        return done(null, userinfo);
    }));

    // --- 3. Routes ---
    
    // Login initiates the OIDC flow
    app.get('/login', passport.authenticate('oidc'));
    
    // Callback handles the response from SurillyaID
    app.get('/callback', passport.authenticate('oidc', {
        successRedirect: '/dashboard',
        failureRedirect: '/login?error=failed'
    }));

    // Protected route
    app.get('/dashboard', (req, res) => {
        if (!req.isAuthenticated()) return res.redirect('/login');
        res.send(`Hello ${req.user.name}! <a href="/logout">Logout</a>`);
    });

    // Logout clears session
    app.get('/logout', (req, res, next) => {
        req.logout(err => {
            if (err) return next(err);
            res.redirect('/');
        });
    });

    app.listen(3000, () => console.log('Server running on http://localhost:3000'));
}

main().catch(console.error);

Python (Flask + Authlib)

This example uses Authlib to handle the OIDC flow in a Flask application.

1. Installation:

pip install Authlib Flask python-dotenv requests

2. Configuration (.env):

Create a .env file in your project root. Do not commit this file to version control.

# Get these from the Surillya Developer Dashboard
SURILLYA_CLIENT_ID=your_client_id_here
SURILLYA_CLIENT_SECRET=your_client_secret_here

# Generate a strong secret key (e.g. `python -c 'import secrets; print(secrets.token_hex())'`)
FLASK_SECRET_KEY=your_generated_secret_key

3. Application Code (app.py):

from flask import Flask, redirect, url_for, session
from authlib.integrations.flask_client import OAuth
import os
from dotenv import load_dotenv

# Load environment variables from .env
load_dotenv()

app = Flask(__name__)

# --- 1. Security Configuration ---
app.secret_key = os.getenv('FLASK_SECRET_KEY')

# Production security headers for cookies
app.config.update(
    SESSION_COOKIE_SECURE=True,    # Requires HTTPS
    SESSION_COOKIE_HTTPONLY=True,  # Prevent XSS access to cookies
    SESSION_COOKIE_SAMESITE='Lax'  # CSRF protection
)

oauth = OAuth(app)

# --- 2. Provider Configuration ---
surillya = oauth.register(
    name='surillya',
    server_metadata_url='https://surillya.com/id/.well-known/openid-configuration',
    client_id=os.getenv('SURILLYA_CLIENT_ID'),
    client_secret=os.getenv('SURILLYA_CLIENT_SECRET'),
    client_kwargs={
        'scope': 'openid profile email',
        'code_challenge_method': 'S256'  # Enforce PKCE
    }
)

# --- 3. Routes ---

@app.route('/')
def homepage():
    user = session.get('user')
    if user:
        return f"Hello, {user['name']}! <a href='/logout'>Logout</a>"
    return '<a href="/login">Login with SurillyaID</a>'

@app.route('/login')
def login():
    # Redirects user to SurillyaID authorization page
    redirect_uri = url_for('auth_callback', _external=True)
    return surillya.authorize_redirect(redirect_uri)

@app.route('/callback')
def auth_callback():
    try:
        # Exchange code for token and get user info
        token = surillya.authorize_access_token()
        user = token['userinfo']
        session['user'] = user
        return redirect('/')
    except Exception as e:
        return f"Authentication failed: {str(e)}"

@app.route('/logout')
def logout():
    session.pop('user', None)
    return redirect('/')

if __name__ == '__main__':
    # WARNING: Do not use `app.run` in production. 
    # Use a WSGI server like Gunicorn: `gunicorn -w 4 app:app`
    app.run(port=5000)

PHP (jumbojett/openid-connect-php)

This example shows a secure structure preventing public access to credentials.

1. Directory Structure:

project/
├── .env                # Credentials (NOT PUBLIC)
├── vendor/             # Dependencies
├── surillya-init.php    # Configuration
├── surillya-protect.php # Include to force login
├── public/             # Web root
│   ├── index.php
│   ├── surillya-login.php
│   └── surillya-callback.php
└── composer.json

2. Installation:

composer require jumbojett/openid-connect-php vlucas/phpdotenv

3. surillya-init.php (Configuration):

<?php
require __DIR__ . '/vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
use Dotenv\Dotenv;

$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();

// Secure session params
session_set_cookie_params([
    'lifetime' => 86400,
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax'
]);

session_start();

$oidc = new OpenIDConnectClient(
    'https://surillya.com/id',
    $_ENV['SURILLYA_CLIENT_ID'],
    $_ENV['SURILLYA_CLIENT_SECRET']
);

$oidc->setRedirectURL('http://localhost:8000/surillya-callback.php');
$oidc->addScope(['openid', 'profile', 'email']);
$oidc->setCodeChallengeMethod('S256');
?>

4. surillya-login.php (Login Endpoint):

<?php
require dirname(__DIR__) . '/surillya-init.php';
$oidc->authenticate(); // Redirects to Surillya Login
?>

5. surillya-callback.php (Callback Handler):

<?php
require dirname(__DIR__) . '/surillya-init.php';

// Process the callback
$oidc->authenticate();

// Save user info
$_SESSION['user'] = $oidc->requestUserInfo();

header('Location: /index.php');
exit;
?>

6. surillya-protect.php (Protection Prefix):

<?php
require_once __DIR__ . '/surillya-init.php';

if (!isset($_SESSION['user'])) {
    header('Location: /surillya-login.php');
    exit;
}
?>

7. index.php (Public Page):

<?php
require dirname(__DIR__) . '/surillya-init.php';
?>

<?php if (isset($_SESSION['user'])): ?>
    <h1>Hello, <?= htmlspecialchars($_SESSION['user']->name) ?></h1>
    <p>Email: <?= htmlspecialchars($_SESSION['user']->email) ?></p>
<?php else: ?>
    <h1>Welcome</h1>
    <a href="/surillya-login.php">Login with SurillyaID</a>
<?php endif; ?>

Need Help?

If you have questions or need assistance: