Inizia con login OIDC standard e account self-service familiari. Aggiungi profili gestiti solo quando il tuo prodotto ha bisogno di persone a carico, gestione delegata o continuita del profilo.
Esegui un flusso CLI per creare un tenant, provisionare i client e scaricare sample pronti con .env precompilato.
npx manage-tuurio-id@1.7.0
Per template server-side con webhook: prima deploy, poi aggiorna l'URL endpoint webhook nella pagina admin webhook del tenant.
MCP Streamable HTTP usa OAuth nel browser, PKCE, token vincolati alle risorse e i permessi effettivi dell'utente connesso.
codex mcp add tuurio-auth --url https://<tenant>.id.tuurio.com/mcp
codex mcp login tuurio-auth --scopes mcp:connect,mcp:write
Per la sola lettura, ometti mcp:write. Gli host con registrazione dinamica compatibile non richiedono un client creato in anticipo.
Il repository pubblico auth_samples offre app di riferimento per associazioni, scuole, portali membri e strumenti interni. Il codice resta su GitHub; questa pagina e il punto di ingresso per gli stack che il tuo team consegna davvero.
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}`);
});
Nota: il secret dovrebbe avere almeno 32 caratteri, altrimenti la libreria potrebbe rifiutare l'avvio.
end_session_endpoint tramite /.well-known/openid-configuration.
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}")
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();
}
// 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();
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()
});
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();
import NextAuth from "next-auth";
const handler = NextAuth({
providers: [
{
id: "tuurio",
name: "Tuurio",
type: "oidc",
issuer: "https://{tenant}.id.tuurio.com",
clientId: "CLIENT_ID_FROM_DASHBOARD",
clientSecret: "CLIENT_SECRET"
}
],
callbacks: {
async jwt({ token, account }) {
// NextAuth validates the OIDC response before exposing account.id_token.
if (account?.id_token) return { ...token, idToken: account.id_token };
return token;
}
}
});
export { handler as GET, handler as POST };
// app/api/logout/route.ts
import { getToken } from "next-auth/jwt";
import type { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
const idTokenHint = typeof token?.idToken === "string" ? token.idToken : null;
if (!idTokenHint) return Response.redirect(new URL('/', request.url));
const issuer = "https://{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'
});
return Response.redirect(`${endSession}?${params}`);
}
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)
}
Suggerimento: AppAuth puo usare la discovery URL (/.well-known/openid-configuration) per evitare endpoint configurati a mano.
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)
}
Suggerimento: anche iOS AppAuth supporta discovery per caricare automaticamente la configurazione.
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',
));
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()
}
$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);
Molti prodotti possono iniziare con il login self-service. Se in seguito genitori, tutori, staff o membri responsabili devono agire per conto di qualcun altro, Tuurio puo modellarlo senza account fittizi.
Mantieni i flussi OIDC per le credenziali mentre archivi separatamente il profilo reale dove le operazioni lo richiedono.
Crea prima i profili di membri, studenti, persone a carico o volontari che non devono ancora ricevere il proprio login.
Se un profilo gestito riceve piu avanti il proprio accesso, lo stesso profilo conserva relazioni e storia.
Definisci i tuoi permessi (per esempio inventory:write o reports:view) direttamente nel dashboard Tuurio.
L'autorizzazione avviene nel token. Non nel tuo database.
Spring Security si aspetta authorities con prefisso SCOPE_ per default. Poiche Tuurio consegna i diritti nel claim permissions, usa un JwtAuthenticationConverter cosi @PreAuthorize("hasAuthority('inventory:write')") funziona senza prefisso.
permissions come array di stringhe.
{
"sub": "user_12345",
"iss": "https://dein-tenant.id.tuurio.com",
"permissions": [
"inventory:write",
"reports:view"
],
"roles": ["ADMIN"]
}
Tuurio fornisce anche claim standard come email_verified o preferred_username, cosi non devi duplicarli nel database.
@PreAuthorize("hasAuthority('inventory:write')")
@PostMapping("/inventory")
public void updateStock() {
// Tuurio hat's erlaubt!
}
@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
}
}
Documentazione di riferimento per gli endpoint di integrazione destinati ai client API usati dagli sviluppatori.
Apri il riferimento API ->Best practice per archiviazione dei token, PKCE e protezione CSRF.
Leggi la guida ->