Skip to content

SSO Integration

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.

High-level authentication flow (Authorization Code)

Section titled “High-level authentication flow (Authorization Code)”

The SSO flow follows the OpenID Connect Authorization Code grant. At a high level:

  1. Redirect to authorize — when the user opens your app from within the Untis Platform, your app redirects the user’s browser to the Untis Platform authorize endpoint
  2. User is authenticated — the Untis Platform confirms the user’s active session (the user is already logged in to the Untis Platform)
  3. Return with code — the Untis Platform redirects back to your configured redirect URI with a short-lived authorization code
  4. Exchange code for tokens — your backend exchanges the code at the token endpoint and receives an ID token and access token
  5. Establish user session — your application reads the sub claim from the ID token to identify the user and establishes its own session
sequenceDiagram
    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=1234

Important:

  • If there is no active Untis Platform session, the user is redirected to the Untis Platform login page. After entering the correct credentials, the user is redirected back to your application automatically
  • 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 error
  • Do not include the untis-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 details
  • Generate a unique nonce per request to prevent replay attacks
POST {API_URL}/WebUntis/api/sso/v3/{tenantId}/token
Content-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:

Terminal window
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:

  • The request header must include Content-Type: application/x-www-form-urlencoded
  • For the Authorization Code flow, use the OIDC client secret — not the platform-generated password (the password is only used for the Client Credentials flow)
  • The response JWT contains the sub claim identifying the user

SSO 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.

Discovering endpoint URLs (well-known configuration)

Section titled “Discovering endpoint URLs (well-known configuration)”

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-configuration

Use 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:

ContextFlowUse caseCredentials used
User contextAuthorization Code (this guide)User-facing UI launched from the platformOIDC client secret
Service contextClient CredentialsServer-to-server API calls, no user sessionPlatform-generated password

Both flows use the same token endpoint (/v3/{tenantId}/token) but with different grant_type values and different credentials.

Service context (API access — Client Credentials)

Section titled “Service context (API access — Client 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_credentials
  • Header: Authorization: Basic {base64(client_id:password)} — use the platform-generated password, not the OIDC secret
  • Header: Content-Type: application/x-www-form-urlencoded
  • Tokens are valid for 3 minutes (check expires_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.

OidcDiscoveryClient.java
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 request and exchange the code

Section titled “Build the authorization request and exchange the code”

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.

OidcAuthorizationClient.java
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.

IdTokenParser.java
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());
}
}