Developer resources

Standards-first identity in minutes.

Start with standard OIDC login and familiar self-service accounts. Add managed profiles only when your product needs dependents, delegated handling, or profile continuity.

Use standard OIDC first. Add managed profiles only when the product model requires them.
manage-tuurio-id

Free production-ready identity templates using Tuurio ID

Run one CLI flow to create a tenant, provision clients, and download ready-to-run samples with prefilled .env values.

Quickstart command ~5 minutes
npx manage-tuurio-id@1.1.6
What happens in the CLI
  1. Choose Login, New, or Website.
  2. New creates tenant + admin and exchanges token automatically.
  3. Create SPA or server-side web app clients.
  4. Optionally enable webhook setup for server-side templates.
  5. Download matching sample from GitHub and auto-write .env.
Included templates
React SPA Vue SPA Angular SPA Node.js Python Java / Spring Go PHP

For webhook-enabled server-side templates: deploy first, then update the webhook endpoint URL in the tenant admin webhook page.

Model Context Protocol

Give AI assistants controlled access to Tuurio

Streamable HTTP MCP uses browser OAuth, PKCE, resource-bound tokens and the signed-in user's effective permissions.

  • Use /mcp for normal tenant operations.
  • Use /mcp/setup only for guided onboarding.
  • Request mcp:write explicitly when approved changes are required.
Full MCP guide
CodexOAuth + PKCE
codex mcp add tuurio-auth --url https://<tenant>.id.tuurio.com/mcp
codex mcp login tuurio-auth --scopes mcp:connect,mcp:write

For read-only access, omit mcp:write. Hosts with compatible dynamic registration do not need a client created in advance.

Example integrations

Start from a working sample, not an abstract snippet

The public auth_samples repository gives you reference apps for clubs, schools, member portals, and internal tools. Keep the code on GitHub, and use this page as the entry point for the stacks your team actually ships.

Open full repository

Web SPAs

Public-client samples for React, Vue 3, Angular, and Next.js with Authorization Code + PKCE.

Server-side web apps

Confidential-client and session-based samples for Node.js, Python, Java, Go, PHP, and Laravel.

Mobile and native

Android, iOS, and Flutter samples for deep-link and app-link sign-in flows.

Choose your stack

All examples are production-oriented
server.js (Express + express-openid-connect) npm install express-openid-connect

const { auth } = require('express-openid-connect');

const config = {
  authRequired: false,
  auth0Logout: true,
  secret: 'YOUR_LONG_RANDOM_STRING',
  baseURL: 'http://localhost:3000',
  clientID: 'CLIENT_ID_FROM_DASHBOARD',
  issuerBaseURL: 'https://{your-tenant}.id.tuurio.com',
  // Logout requires the ID token from the validated login session.
};

// Add the auth middleware and you are done.
app.use(auth(config));

app.get('/', (req, res) => {
  res.send(req.oidc.isAuthenticated() ? 'Signed in as ' + req.oidc.user.name : 'Not signed in');
});

// Logout (OIDC RP-initiated)
app.get('/logout', async (req, res) => {
  const idTokenHint = req.oidc.idToken;
  if (!idTokenHint) return res.redirect('/');
  const issuer = "https://{your-tenant}.id.tuurio.com";
  const discovery = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());
  const endSession = discovery.end_session_endpoint;
  const params = new URLSearchParams({
    client_id: 'CLIENT_ID_FROM_DASHBOARD',
    id_token_hint: idTokenHint,
    post_logout_redirect_uri: 'https://example.com/logout/success'
  });
  res.redirect(`${endSession}?${params}`);
});
                        

Note: the secret should be at least 32 characters long, otherwise the library may refuse to start.

Tip: discover the end_session_endpoint automatically via /.well-known/openid-configuration.
app.py (Flask + Authlib) pip install Authlib Flask Flask-Session redis requests

import os
from authlib.integrations.flask_client import OAuth
from flask_session import Session
from redis import Redis
import requests
from urllib.parse import urlencode

# Flask's default session is a signed browser cookie. Use a server-side store
# before retaining the validated ID token for RP-initiated logout.
app.config.update(
    SESSION_TYPE="redis",
    SESSION_REDIS=Redis.from_url(os.environ["REDIS_URL"]),
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
)
Session(app)

oauth = OAuth(app)
oauth.register(
    name='tuurio',
    client_id='CLIENT_ID_FROM_DASHBOARD',
    client_secret='CLIENT_SECRET',
    server_metadata_url='https://{tenant}.id.tuurio.com/.well-known/openid-configuration',
    # Keep the validated ID token in the encrypted server session for logout.
    client_kwargs={'scope': 'openid profile email'}
)

@app.route('/login')
def login():
    redirect_uri = url_for('callback', _external=True)
    return oauth.tuurio.authorize_redirect(redirect_uri)

@app.route('/callback')
def callback():
    token = oauth.tuurio.authorize_access_token()
    session["tuurio_id_token"] = token["id_token"]
    user = token['userinfo']
    return f'Hello, {user["name"]}'

@app.route('/logout')
def logout():
    id_token_hint = session.pop("tuurio_id_token", None)
    if not id_token_hint:
        return redirect(url_for("index"))
    discovery = requests.get("https://{tenant}.id.tuurio.com/.well-known/openid-configuration").json()
    end_session = discovery["end_session_endpoint"]
    params = urlencode({
        "client_id": "CLIENT_ID_FROM_DASHBOARD",
        "id_token_hint": id_token_hint,
        "post_logout_redirect_uri": "https://example.com/logout/success",
    })
    return redirect(f"{end_session}?{params}")
                        
application.yml (Spring Boot Starter OAuth2 Client) No manual code required!

spring:
  security:
    oauth2:
      client:
        registration:
          tuurio:
            client-id: CLIENT_ID_FROM_DASHBOARD
            client-secret: CLIENT_SECRET
            scope: [openid, profile, email]
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            # Spring's OIDC logout handler supplies the validated ID token hint.
        provider:
          tuurio:
            issuer-uri: https://{your-tenant}.id.tuurio.com
                        

@Bean
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler(
    ClientRegistrationRepository registrations) {
  var handler = new OidcClientInitiatedLogoutSuccessHandler(registrations);
  handler.setPostLogoutRedirectUri("https://example.com/logout/success");
  return handler;
}

@Bean
SecurityFilterChain securityFilterChain(
    HttpSecurity http,
    OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler) throws Exception {
  http
      .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults())
      .logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler));
  return http.build();
}
                        
Note: This example shows a public client with PKCE. For production web apps, we recommend a BFF (Backend for Frontend) so tokens stay on the server. Details in the Security guide.
auth.ts (React SPA + oidc-client-ts, PKCE) npm install oidc-client-ts

// end_session_endpoint via discovery:
// const issuer = "https://{tenant}.id.tuurio.com";
// const discovery = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());
// const endSessionEndpoint = discovery.end_session_endpoint;
                        

import { UserManager } from "oidc-client-ts";

const mgr = new UserManager({
  authority: "https://{tenant}.id.tuurio.com",
  client_id: "CLIENT_ID_FROM_DASHBOARD",
  redirect_uri: "http://localhost:5173/auth/callback",
  post_logout_redirect_uri: "http://localhost:5173/",
  response_type: "code",
  scope: "openid profile email",
  automaticSilentRenew: true
});

export const login = () => mgr.signinRedirect();
export const handleCallback = () => mgr.signinRedirectCallback();
// Logout uses end_session_endpoint from discovery
export const logout = () => mgr.signoutRedirect();
                        
Note: This example shows a public client with PKCE. For production web apps, we recommend a BFF (Backend for Frontend) so tokens stay on the server. Details in the Security guide.
useAuth.ts (Vue 3 + oidc-client-ts) npm install oidc-client-ts

import { UserManager } from "oidc-client-ts";

const mgr = new UserManager({
  authority: "https://{tenant}.id.tuurio.com",
  client_id: "CLIENT_ID_FROM_DASHBOARD",
  redirect_uri: "http://localhost:5173/auth/callback",
  post_logout_redirect_uri: "http://localhost:5173/",
  response_type: "code",
  scope: "openid profile email"
});

export const useAuth = () => ({
  login: () => mgr.signinRedirect(),
  handleCallback: () => mgr.signinRedirectCallback(),
  // Logout uses end_session_endpoint from discovery
  logout: () => mgr.signoutRedirect()
});
                        
Note: This example shows a public client with PKCE. For production web apps, we recommend a BFF (Backend for Frontend) so tokens stay on the server. Details in the Security guide.
auth.config.ts (Angular + angular-oauth2-oidc) npm install angular-oauth2-oidc

import { AuthConfig, OAuthService } from "angular-oauth2-oidc";

export const authConfig: AuthConfig = {
  issuer: "https://{tenant}.id.tuurio.com",
  clientId: "CLIENT_ID_FROM_DASHBOARD",
  redirectUri: window.location.origin + "/auth/callback",
  postLogoutRedirectUri: "http://localhost:5173/",
  responseType: "code",
  scope: "openid profile email"
};

export const initLogout = async (oauthService: OAuthService) => {
  const discovery = await fetch(`${authConfig.issuer}/.well-known/openid-configuration`).then(r => r.json());
  oauthService.logoutUrl = discovery.end_session_endpoint;
};

export const logout = (oauthService: OAuthService) => oauthService.logOut();
                        
Android (Kotlin) + AppAuth OAuth 2.0 + PKCE (Public Client)

val serviceConfig = AuthorizationServiceConfiguration(
    Uri.parse("https://{tenant}.id.tuurio.com/oauth2/authorize"),
    Uri.parse("https://{tenant}.id.tuurio.com/oauth2/token")
)

val request = AuthorizationRequest.Builder(
    serviceConfig,
    "CLIENT_ID_FROM_DASHBOARD",
    ResponseTypeValues.CODE,
    Uri.parse("com.example.app:/oauth2redirect")
)
    .setScope("openid profile email")
    .build()

// val postLogoutRedirectUri = Uri.parse("com.example.app:/logout")

val authService = AuthorizationService(context)
val intent = authService.getAuthorizationRequestIntent(request)
startActivityForResult(intent, RC_AUTH)

// Logout uses the validated ID token retained in AuthState.
AuthorizationServiceConfiguration.fetchFromUrl(
    Uri.parse("https://{tenant}.id.tuurio.com/.well-known/openid-configuration")
) { config, _ ->
    val idTokenHint = authState.lastTokenResponse?.idToken ?: return@fetchFromUrl
    val endSession = EndSessionRequest.Builder(config!!)
        .setIdTokenHint(idTokenHint)
        .setPostLogoutRedirectUri(Uri.parse("com.example.app:/logout"))
        .build()
    val endSessionIntent = authService.getEndSessionRequestIntent(endSession)
    startActivityForResult(endSessionIntent, RC_LOGOUT)
}
                        

Tip: AppAuth can use the discovery URL (/.well-known/openid-configuration) so you do not have to hardcode endpoints.

iOS (Swift) + AppAuth OAuth 2.0 + PKCE (Public Client)

let config = OIDServiceConfiguration(
  authorizationEndpoint: URL(string: "https://{tenant}.id.tuurio.com/oauth2/authorize")!,
  tokenEndpoint: URL(string: "https://{tenant}.id.tuurio.com/oauth2/token")!
)

let request = OIDAuthorizationRequest(
  configuration: config,
  clientId: "CLIENT_ID_FROM_DASHBOARD",
  scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail],
  redirectURL: URL(string: "com.example.app:/oauth2redirect")!,
  responseType: OIDResponseTypeCode,
  additionalParameters: nil
)

// let postLogoutRedirectURL = URL(string: "com.example.app:/logout")!

OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in
  // Store authState?.lastTokenResponse?.accessToken
}

// Logout uses the validated ID token retained in AuthState.
OIDAuthorizationService.discoverConfiguration(
  forIssuer: URL(string: "https://{tenant}.id.tuurio.com")!
) { config, _ in
  guard let config = config else { return }
  let endSession = OIDEndSessionRequest(
    configuration: config,
    idTokenHint: authState?.lastTokenResponse?.idToken,
    postLogoutRedirectURL: URL(string: "com.example.app:/logout")!,
    additionalParameters: nil
  )
  self.present(OIDAuthorizationService.present(endSession, presenting: self) { _, _ in }, animated: true)
}
                        

Tip: iOS AppAuth also supports discovery to load the configuration automatically.

Flutter (Dart) + flutter_appauth flutter pub add flutter_appauth

import 'package:flutter_appauth/flutter_appauth.dart';

final appAuth = FlutterAppAuth();

final result = await appAuth.authorizeAndExchangeCode(
  AuthorizationTokenRequest(
    'CLIENT_ID_FROM_DASHBOARD',
    'com.example.app:/oauth2redirect',
    issuer: 'https://{tenant}.id.tuurio.com',
    scopes: ['openid', 'profile', 'email'],
  ),
);

// Logout (OIDC RP-initiated)
await appAuth.endSession(EndSessionRequest(
  idTokenHint: result?.idToken,
  postLogoutRedirectUrl: 'com.example.app:/logout',
  issuer: 'https://{tenant}.id.tuurio.com',
));
                        
main.go (golang.org/x/oauth2) go get golang.org/x/oauth2

var config = &oauth2.Config{
    ClientID:     "CLIENT_ID",
    ClientSecret: "CLIENT_SECRET",
    RedirectURL:  "http://localhost:3000/callback",
    Scopes:       []string{"openid", "profile", "email"},
    // Retain the validated ID token in the server-side session for logout.
    Endpoint: oauth2.Endpoint{
        AuthURL:  "https://{tenant}.id.tuurio.com/oauth2/authorize",
        TokenURL: "https://{tenant}.id.tuurio.com/oauth2/token",
    },
}
// Nutze config.AuthCodeURL(...) und config.Exchange(...)

// Logout (OIDC RP-initiated). idTokenHint comes from validated server session state.
func logoutURL(idTokenHint string) string {
    if idTokenHint == "" { return "/" }
    resp, _ := http.Get("https://{tenant}.id.tuurio.com/.well-known/openid-configuration")
    defer resp.Body.Close()
    var discovery struct{ EndSessionEndpoint string `json:"end_session_endpoint"` }
    json.NewDecoder(resp.Body).Decode(&discovery)
    values := url.Values{
        "client_id": {"CLIENT_ID_FROM_DASHBOARD"},
        "id_token_hint": {idTokenHint},
        "post_logout_redirect_uri": {"https://example.com/logout/success"},
    }
    return discovery.EndSessionEndpoint + "?" + values.Encode()
}
                        
index.php (league/oauth2-client) composer require league/oauth2-client

$provider = new \League\OAuth2\Client\Provider\GenericProvider([
    'clientId'                => 'CLIENT_ID',
    'clientSecret'            => 'CLIENT_SECRET',
    'redirectUri'             => 'https://example.com/callback',
    'urlAuthorize'            => 'https://{tenant}.id.tuurio.com/oauth2/authorize',
    'urlAccessToken'          => 'https://{tenant}.id.tuurio.com/oauth2/token',
    'urlResourceOwnerDetails' => 'https://{tenant}.id.tuurio.com/userinfo',
    // Retain the validated ID token in the server-side PHP session for logout.
]);

// Logout (OIDC RP-initiated)
$discovery = json_decode(file_get_contents("https://{tenant}.id.tuurio.com/.well-known/openid-configuration"), true);
$endSession = $discovery['end_session_endpoint'];
$idTokenHint = $_SESSION['tuurio_id_token'] ?? '';
unset($_SESSION['tuurio_id_token']);
if ($idTokenHint === '') {
    header('Location: /');
    exit;
}
$params = http_build_query([
    'client_id' => 'CLIENT_ID_FROM_DASHBOARD',
    'id_token_hint' => $idTokenHint,
    'post_logout_redirect_uri' => 'https://example.com/logout/success',
]);
header('Location: ' . $endSession . '?' . $params);
                        

Model real people, not only accounts

Many products can start with self-service login. If parents, guardians, staff, or responsible members later need to act for someone else, Tuurio can model that without forcing fake accounts.

Account and profile are not always the same

Keep OIDC login flows for credentials while storing the real profile separately where operations need it.

Managed profiles without credentials

Create profiles first for members, students, dependents, or volunteers who should not yet receive their own login.

Continuity when login is added later

If a managed profile later receives its own access, the same profile continues with the same relationships and history.

Built-in access control

Define your own permissions (for example inventory:write or reports:view) directly in the Tuurio dashboard.

Authorization happens in the token. Not in your database.

Spring Security expects authorities with the SCOPE_ prefix by default. Because Tuurio delivers rights in the permissions claim, use a JwtAuthenticationConverter so @PreAuthorize("hasAuthority('inventory:write')") works without a prefix.

Permissions are delivered in the permissions claim as an array of strings.
Decoded JWT (Access Token)

{
  "sub": "user_12345",
  "iss": "https://dein-tenant.id.tuurio.com",
  "permissions": [
    "inventory:write",
    "reports:view"
  ],
  "roles": ["ADMIN"]
}
                                

Tuurio also delivers standard claims such as email_verified or preferred_username, so you do not need to duplicate them in your database.

Backend example (Spring Security)

@PreAuthorize("hasAuthority('inventory:write')")
@PostMapping("/inventory")
public void updateStock() {
    // Tuurio hat's erlaubt!
}
                                    
SecurityConfig (Kotlin)

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .oauth2ResourceServer { oauth2 ->
                oauth2.jwt { jwt ->
                    jwt.jwtAuthenticationConverter(tuurioAuthenticationConverter())
                }
            }
        return http.build()
    }

    private fun tuurioAuthenticationConverter(): Converter {
        val converter = JwtAuthenticationConverter()
        converter.setJwtGrantedAuthoritiesConverter { jwt ->
            // Extrahiert das "permissions" Array aus dem Token
            val permissions = jwt.getClaim>("permissions") ?: emptyList()

            // Mapping zu SimpleGrantedAuthority
            permissions.map { SimpleGrantedAuthority(it) }
        }
        return converter
    }
}
                                    
API reference

Reference docs for the integration endpoints that are meant to be consumed by developer-facing API clients.

Open API reference ->
Security guide

Best practices for token storage, PKCE, and CSRF protection.

Read guide ->
Support

Blocked on integration? We will debug with you.

Contact support ->