Commencez avec un login OIDC standard et des comptes self-service familiers. Ajoutez des profils geres seulement lorsque votre produit a besoin de personnes a charge, de gestion deleguee ou de continuite de profil.
Lancez un flux CLI pour creer un tenant, provisionner des clients et telecharger des samples prets a l'emploi avec .env pre-rempli.
npx manage-tuurio-id@1.7.0
Pour les templates server-side avec webhooks : deployez d'abord, puis mettez a jour l'URL d'endpoint webhook dans la page admin webhook du tenant.
MCP Streamable HTTP utilise OAuth dans le navigateur, PKCE, des jetons liés aux ressources et les droits effectifs de l'utilisateur connecté.
codex mcp add tuurio-auth --url https://<tenant>.id.tuurio.com/mcp
codex mcp login tuurio-auth --scopes mcp:connect,mcp:write
Pour la lecture seule, omettez mcp:write. Les hôtes avec enregistrement dynamique compatible n'exigent pas de client créé à l'avance.
Le depot public auth_samples fournit des applications de reference pour associations, ecoles, portails membres et outils internes. Le code reste sur GitHub; cette page sert de point d'entree pour les stacks que votre equipe livre reellement.
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 : le secret doit comporter au moins 32 caracteres, sinon la bibliotheque peut refuser de demarrer.
end_session_endpoint via /.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)
}
Conseil : AppAuth peut utiliser l'URL de discovery (/.well-known/openid-configuration) afin d'eviter de fixer les endpoints en dur.
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)
}
Conseil : iOS AppAuth prend egalement en charge discovery pour charger automatiquement la configuration.
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);
Beaucoup de produits peuvent commencer avec un login self-service. Si vous devez plus tard laisser des parents, tuteurs, equipes ou membres responsables agir pour quelqu'un, Tuurio peut le modeliser sans faux comptes.
Gardez les flux OIDC pour les credentials tout en stockant separement le vrai profil la ou les operations en ont besoin.
Creez d'abord les profils de membres, d'eleves, de personnes a charge ou de benevoles qui ne doivent pas encore recevoir leur propre connexion.
Si un profil gere recoit plus tard son propre acces, le meme profil conserve les memes relations et le meme historique.
Definissez vos propres permissions (par ex. inventory:write ou reports:view) directement dans le dashboard Tuurio.
L'autorisation se fait dans le token. Pas dans votre base de donnees.
Spring Security attend par defaut des authorities avec le prefixe SCOPE_. Comme Tuurio livre les droits dans le claim permissions, utilisez un JwtAuthenticationConverter pour que @PreAuthorize("hasAuthority('inventory:write')") fonctionne sans prefixe.
permissions sous forme de tableau de chaines.
{
"sub": "user_12345",
"iss": "https://dein-tenant.id.tuurio.com",
"permissions": [
"inventory:write",
"reports:view"
],
"roles": ["ADMIN"]
}
Tuurio fournit aussi des claims standard comme email_verified ou preferred_username, afin d'eviter de les dupliquer dans votre base de donnees.
@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
}
}
Documentation de reference pour les points d'integration destines aux clients API cote developpeur.
Ouvrir la reference API ->Bonnes pratiques pour le stockage des tokens, PKCE et la protection CSRF.
Lire le guide ->