Untis Platform as IdP
This guide covers this scenario. The Untis Platform authenticates the user and issues tokens to your application. Used when your UI is integrated via the platform.
SSO integration provides a “one login” experience: when a user opens your application from within the Untis Platform, they are authenticated automatically — no separate login screen in your app is required. The Untis Platform acts as the identity provider and your application receives a token identifying the user.
SSO is always used together with UI Integration. UI integration defines the entry point (where your app is launched from); SSO handles authentication when the user arrives.
Use SSO when your application needs to know who the user is — i.e., when the integration is user-context. This covers the typical UI integration scenario: a teacher or student clicks your app in the Untis Platform and your application needs to identify them, load their data, or personalize the experience.
If your integration is server-to-server only (no user-facing UI launched from within the platform), SSO is not needed. Use the Client Credentials flow instead.
Untis Platform as IdP
This guide covers this scenario. The Untis Platform authenticates the user and issues tokens to your application. Used when your UI is integrated via the platform.
Untis Platform as SP
The Untis Platform accepts sign-ins from an external identity provider — your app is the IdP. Configured separately by school administrators.
The SSO flow follows the OpenID Connect Authorization Code grant. At a high level:
sub claim from the ID token to identify the user and establishes its own sessionsequenceDiagram
autonumber
participant User
participant PA as Partner App
participant WU as Untis Platform
User->>PA: Opens app from within Untis Platform
PA->>WU: GET /v3/{tenantId}/authorize
WU-->>PA: 302 redirect with ?code=...
PA->>WU: POST /v3/{tenantId}/token
WU-->>PA: id_token + access_token
PA-->>User: Authenticated app content
GET {API_URL}/WebUntis/api/sso/v3/{tenantId}/authorize ?response_type=code &scope=roster-core.readonly openid &client_id={OIDC_CLIENT_ID} &redirect_uri={YOUR_REDIRECT_URI} &nonce={NONCE}Example:
GET https://api.integration.webuntis.dev/WebUntis/api/sso/v3/1234/authorize?response_type=code&scope=roster-core.readonly%20openid&client_id=BestApp&redirect_uri=bestapp.example.domain.at/redirect&nonce=1234Important:
redirect_uri must exactly match the domain and SSO redirect path configured for your platform application in PAM — a mismatch results in an invalid_resource erroruntis-profile scope. It is not backwards-compatible; changes to it may require development effort on your side. Use only sub from the token and call the OneRoster users endpoint to retrieve user detailsnonce per request to prevent replay attacksPOST {API_URL}/WebUntis/api/sso/v3/{tenantId}/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code={AUTHORIZATION_CODE}&redirect_uri={YOUR_REDIRECT_URI}&client_id={OIDC_CLIENT_ID}&client_secret={OIDC_CLIENT_SECRET}Example:
curl -X POST "https://api.integration.webuntis.dev/WebUntis/api/sso/v3/1234/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=HmToMB_iITpHLucs8RvrL8pEi6iizoKKK7_W8lJSi0k" \ -d "redirect_uri=bestapp.example.domain.at/redirect" \ -d "client_id=BestApp" \ -d "client_secret=Rd5kweLWyww9TYPjjrrvCq3MnCnzezuUs"Important:
Content-Type: application/x-www-form-urlencodedsub claim identifying the userSSO relies on your platform application being correctly configured in PAM. A mismatch in any of these will cause the authorize endpoint to return an error.
OIDC client ID
Used as client_id in both the authorize and token requests.
OIDC client secret
Used as client_secret in the token request (Authorization Code flow only).
SSO redirect path
The path the Untis Platform redirects back to after authentication. Must
exactly match the redirect_uri in your authorize request.
Domain
Your application’s domain must be registered in PAM. A domain mismatch
causes an invalid_resource error.
See Get Your Platform Application for how these are configured during registration.
The Untis Platform exposes an OpenID Connect discovery endpoint that returns all endpoint URLs and supported configuration values for a given tenant:
GET {API_URL}/WebUntis/api/sso/v3/{tenantId}/.well-known/openid-configurationUse this to programmatically resolve the authorize and token endpoint URLs rather than hardcoding them.
There are two distinct token contexts in the Untis Platform, obtained through different flows:
| Context | Flow | Use case | Credentials used |
|---|---|---|---|
| User context | Authorization Code (this guide) | User-facing UI launched from the platform | OIDC client secret |
| Service context | Client Credentials | Server-to-server API calls, no user session | Platform-generated password |
Both flows use the same token endpoint (/v3/{tenantId}/token) but with different grant_type values and different credentials.
For server-to-server integrations that call APIs without a user session:
POST {API_URL}/WebUntis/api/sso/v3/{tenantId}/token ?grant_type=client_credentialsAuthorization: Basic {base64(client_id:password)} — use the platform-generated password, not the OIDC secretContent-Type: application/x-www-form-urlencodedexpires_in in the response and avoid unnecessary token requests)See Authentication Model for full details.
The following examples use Spring RestClient (Spring Framework 6.1+). No Spring Boot auto-configuration is assumed — the client can be used in any Java application.
Resolve the authorize and token endpoint URLs for a specific tenant dynamically instead of hardcoding them.
import org.springframework.web.client.RestClient;import java.util.Map;
public class OidcDiscoveryClient {
private static final String API_URL = "https://api.integration.webuntis.dev";
private final RestClient http = RestClient.create();
public Map<String, Object> discover(String tenantId) { String url = API_URL + "/WebUntis/api/sso/v3/" + tenantId + "/.well-known/openid-configuration"; return http.get().uri(url).retrieve().body(Map.class); }}Build the authorization redirect URL (using the discovered authorization_endpoint from the previous step), then exchange the returned code for tokens at the token_endpoint.
10 collapsed lines
import org.springframework.http.HttpHeaders;import org.springframework.http.MediaType;import org.springframework.util.LinkedMultiValueMap;import org.springframework.util.MultiValueMap;import org.springframework.web.client.RestClient;import org.springframework.web.util.UriComponentsBuilder;import java.security.SecureRandom;import java.util.Base64;import java.util.Map;
public class OidcAuthorizationClient {
public String buildRedirectUrl(String authorizationEndpoint, String clientId, String redirectUri, String state, String nonce) { return UriComponentsBuilder.fromUriString(authorizationEndpoint) .queryParam("response_type", "code") .queryParam("scope", "roster-core.readonly openid") .queryParam("client_id", clientId) .queryParam("redirect_uri", redirectUri) .queryParam("state", state) .queryParam("nonce", nonce) .build() .encode() .toUriString(); }
/** Generates a cryptographically random string for use as state or nonce. */ public static String generateSecureString() { byte[] bytes = new byte[32]; new SecureRandom().nextBytes(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); }}
class TokenExchangeClient {
private final RestClient http = RestClient.create();
public Map<String, Object> exchangeCode(String tokenEndpoint, String code, String clientId, String clientSecret, String redirectUri) { MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.add("grant_type", "authorization_code"); form.add("code", code); form.add("redirect_uri", redirectUri); form.add("client_id", clientId); form.add("client_secret", clientSecret);
return http.post() .uri(tokenEndpoint) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) .body(form) .retrieve() .body(Map.class); }}Decode the returned id_token JWT, verify the nonce to prevent replay attacks, check expiry, and extract the sub claim to identify the user.
3 collapsed lines
import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import java.security.PublicKey;
public class IdTokenParser {
private final PublicKey signingKey; private final String expectedIssuer; private final String clientId;
/** * @param signingKey the RSA public key from the tenant's JWKS endpoint * ({@code jwks_uri} in the well-known configuration) * @param expectedIssuer the issuer URL from the well-known OIDC configuration * @param clientId your OIDC client ID (expected audience) */ public IdTokenParser(PublicKey signingKey, String expectedIssuer, String clientId) { this.signingKey = signingKey; this.expectedIssuer = expectedIssuer; this.clientId = clientId; }
public record ParsedIdToken(String sub, long exp) {}
public ParsedIdToken parse(String idToken, String expectedNonce) { // JJWT validates the signature, structure, and expiry automatically. Claims claims = Jwts.parser() .verifyWith(signingKey) .requireIssuer(expectedIssuer) .requireAudience(clientId) .build() .parseSignedClaims(idToken) .getPayload();
if (!expectedNonce.equals(claims.get("nonce", String.class))) { throw new SecurityException("Nonce mismatch — possible replay attack"); }
String sub = claims.getSubject(); if (sub == null || sub.isBlank()) { throw new IllegalArgumentException("'sub' claim missing from id_token"); }
return new ParsedIdToken(sub, claims.getExpiration().toInstant().getEpochSecond()); }}