
# Anmeldung in eine Spring-Boot-App hinzufügen

Dieser Schnellstart fügt einer Spring-Boot-4.1-App die KeenAuth-Anmeldung mit dem OAuth-2.0-Client von Spring Security hinzu, dem Standard-OpenID-Connect-Client des Spring-Ökosystems. KeenAuth hat noch kein Java-SDK, und ein solches ist auch nicht nötig: KeenAuth ist ein Standard-OpenID-Connect-Provider. Ihre App ist der OAuth-Client und bewahrt jedes Token in der serverseitigen Sitzung auf; der Browser erhält nur ein Sitzungs-ID-Cookie. Sie konfigurieren die Anmeldung, schützen eine Seite, rufen eine geschützte API mit dem Access Token des Benutzers auf, melden sich ab und testen die App offline.

Die App hat diese Routen:

| Route | Was sie tut |
|---|---|
| `GET /` | Öffentliche Startseite: ein Anmeldelink oder der Name des Benutzers und eine Schaltfläche zum Abmelden |
| `GET /dashboard` | Benötigt eine Sitzung. Zeigt den Benutzer und die Nachrichten, die Ihre API zurückgibt |
| `GET /oauth2/authorization/keenauth` | Von Spring Security bereitgestellt: startet die Anmeldung |
| `GET /login/oauth2/code/keenauth` | Von Spring Security bereitgestellt: der Callback |
| `POST /logout` | Von Spring Security bereitgestellt: beendet die Sitzung hier und bei KeenAuth. Benötigt das CSRF-Token |

## Bevor Sie beginnen

- Eine KeenAuth-Umgebung. Ihr Issuer sieht etwa so aus: `https://your-environment.eu.keenauth.app`.
- Eine Webanwendung in der Umgebung ([Anwendung anlegen](/docs/api/management/applications/create)) mit einem Client Secret (`client_secret_basic`). Registrieren Sie in einer `development`-Umgebung den Redirect-URI `http://localhost/login/oauth2/code/keenauth` und den Post-Logout-Redirect-URI `http://localhost/`; ein Loopback-URI wird ohne Port registriert und passt auf jeden Port, etwa `8080`.
- Eine API, die in der Umgebung registriert ist ([API anlegen](/docs/api/management/apis/create)) mit dem Scope `messages:read`. Ihr `identifier`, zum Beispiel `https://api.example.com`, ist die Audience ihrer Access Tokens. Der [Express-Schnellstart](/docs/quickstarts/express) erstellt eine API, die diese Tokens akzeptiert.
- JDK 21 und Maven 3.9.

## 1. Abhängigkeiten hinzufügen

Das Spring-Boot-Parent (`spring-boot-starter-parent` 4.1.2) legt jede Version fest. Fügen Sie die Starter für Web, OAuth-2.0-Client und Thymeleaf hinzu:

```xml
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webmvc</artifactId>
      <exclusions>
        <!-- Logback is EPL-1.0 OR LGPL-2.1, outside the licence allow list: Log4j 2 (Apache-2.0) logs instead. -->
        <exclusion>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
        <!-- EPL-2.0 OR GPL-2.0 with Classpath Exception, outside the allow list; this app uses none of its annotations. -->
        <exclusion>
          <groupId>jakarta.annotation</groupId>
          <artifactId>jakarta.annotation-api</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    <!-- Spring Security's OAuth 2.0 client: OpenID Connect sign-in with discovery, PKCE and ID token checks. -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
```

Das Beispiel ersetzt zwei Abhängigkeiten, deren Lizenzen nicht auf der Lizenz-Allowlist unserer Beispiele stehen: Logback (EPL-1.0 oder LGPL-2.1) durch Log4j 2 und `jakarta.annotation-api` (EPL-2.0 oder GPL-2.0 mit Classpath Exception), die die App nicht verwendet. Aus demselben Grund verwenden die Tests TestNG statt JUnit (EPL-2.0). Ihre eigene App muss das nicht tun.

## 2. Konfigurieren

Die App liest ihre Konfiguration ausschließlich aus Umgebungsvariablen. Committen Sie deren Werte niemals.

| Variable | Erforderlich | Bedeutung |
|---|---|---|
| `KEENAUTH_ISSUER` | ja | Der Issuer Ihrer Umgebung, `https` |
| `KEENAUTH_CLIENT_ID` | ja | Die `client_id` der Webanwendung |
| `KEENAUTH_CLIENT_SECRET` | ja | Ihr Client Secret |
| `APP_BASE_URL` | ja | Die öffentliche URL der App. Der Redirect-URI wird daraus gebildet. `http` nur für `localhost`, `127.0.0.1` oder `[::1]` |
| `API_URL` | ja | Die API, die die App aufruft, zum Beispiel `http://localhost:3001` |
| `KEENAUTH_API_AUDIENCE` | ja | Der Identifier dieser API in KeenAuth |
| `SESSION_SECURE_COOKIE` | nein | `false` nur für einfaches `http://localhost` |

`KeenAuthSettings.from` liest diese Werte beim Start ein und prüft sie. Fehlt ein Wert oder ist er unbrauchbar, stoppt die App, und keine Fehlermeldung enthält einen Wert.

```java
	public static KeenAuthSettings from(Function<String, String> env) {
		return new KeenAuthSettings(url(need(env, "KEENAUTH_ISSUER"), "KEENAUTH_ISSUER", true, false),
				need(env, "KEENAUTH_CLIENT_ID"), need(env, "KEENAUTH_CLIENT_SECRET"),
				url(need(env, "APP_BASE_URL"), "APP_BASE_URL", false, false),
				url(need(env, "API_URL"), "API_URL", false, false),
				// RFC 8707: the access token is requested for your API, which then accepts it as its audience.
				url(need(env, "KEENAUTH_API_AUDIENCE"), "KEENAUTH_API_AUDIENCE", false, true));
	}
```

## 3. Client registrieren

Die Client-Registrierung stammt aus Discovery (`<issuer>/.well-known/openid-configuration`), das Spring Security beim Start abruft. Das Beispiel setzt die Client-Zugangsdaten, den Redirect-URI und die Scopes und aktiviert PKCE für diesen vertraulichen Client:

```java
	/** The endpoints and the signing keys come from discovery: <issuer>/.well-known/openid-configuration. */
	@Bean
	ClientRegistrationRepository clientRegistrationRepository(KeenAuthSettings settings) {
		return new InMemoryClientRegistrationRepository(
				registration(ClientRegistrations.fromIssuerLocation(settings.issuer()), settings));
	}

	static ClientRegistration registration(ClientRegistration.Builder discovered, KeenAuthSettings settings) {
		return discovered.registrationId("keenauth")
			.clientId(settings.clientId())
			.clientSecret(settings.clientSecret())
			.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
			.redirectUri(settings.redirectUri())
			// messages:read is a scope of your API; the example API requires it on /api/messages.
			.scope("openid", "profile", "email", "messages:read")
			// PKCE (RFC 7636) with S256 for this confidential client too; state and nonce are always on.
			.clientSettings(ClientRegistration.ClientSettings.builder().requireProofKey(true).build())
			.build();
	}
```

## 4. Anmeldung, API-Audience und Abmeldung konfigurieren

Der Resolver für die Autorisierungsanfrage fügt den Identifier Ihrer API als `resource` hinzu (RFC 8707). Ohne ihn ist das Access Token nicht für Ihre API bestimmt, und die API weist es zurück. Die Abmeldung ist das `POST /logout` von Spring Security mit dem CSRF-Token, gefolgt von OpenID Connect RP-Initiated Logout mit `id_token_hint` und `post_logout_redirect_uri`:

```java
		DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(clients,
				"/oauth2/authorization");
		// RFC 8707: an access token for your API.
		resolver.setAuthorizationRequestCustomizer(
				(request) -> request.additionalParameters((params) -> params.put("resource", settings.apiAudience())));

		OidcClientInitiatedLogoutSuccessHandler logout = new OidcClientInitiatedLogoutSuccessHandler(clients);
		logout.setPostLogoutRedirectUri(settings.postLogoutRedirectUri());

		http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/", "/error").permitAll()
			.anyRequest()
			.authenticated())
			.oauth2Login((login) -> login.authorizationEndpoint((endpoint) -> endpoint.authorizationRequestResolver(resolver)))
			// POST /logout with the CSRF token; then OpenID Connect RP-Initiated Logout with id_token_hint.
			.logout((out) -> out.logoutSuccessHandler(logout));
		return http.build();
```

Spring Security prüft `state` beim Callback, tauscht den Code mit dem PKCE-Verifier ein und prüft das ID Token (Signatur mit den Schlüsseln Ihrer Umgebung, RS256, `iss`, `aud`, `exp`, `iat` und `nonce`). Nach der Anmeldung vergibt es dem Benutzer eine neue Sitzungs-ID. Die Sitzung lebt auf dem Server; bei mehreren Servern verwenden Sie Spring Session mit einem gemeinsamen Speicher.

Die Startseite verlinkt zur Anmeldung und sendet ein Formular an die Abmeldung; `th:action` fügt das CSRF-Token hinzu:

```html
      <!-- th:action adds the CSRF token. -->
      <form th:action="@{/logout}" method="post"><button type="submit">Sign out</button></form>
    </div>
    <p th:unless="${user}"><a href="/oauth2/authorization/keenauth">Sign in</a></p>
```

## 5. Ihre API aufrufen

Das Dashboard benötigt eine Sitzung. Es nimmt das Access Token aus dem autorisierten Client, den Spring Security in der Sitzung hält, und ruft damit Ihre API auf. Das Token bleibt auf dem Server; der Browser erhält nur das HTML.

```java
	/** Needs a session (SecurityConfig). The access token stays on the server; the browser receives only the HTML. */
	@GetMapping("/dashboard")
	public String dashboard(@AuthenticationPrincipal OidcUser user,
			@RegisteredOAuth2AuthorizedClient("keenauth") OAuth2AuthorizedClient client, Model model) {
		ResponseEntity<String> response = this.messages.fetch(client.getAccessToken().getTokenValue());
		model.addAttribute("user", user);
		model.addAttribute("status", response.getStatusCode().value());
		model.addAttribute("messages", response.getStatusCode().is2xxSuccessful() ? response.getBody() : null);
		return "dashboard";
	}
```

```java
	public ResponseEntity<String> fetch(String accessToken) {
		return this.rest.get()
			.uri("/api/messages")
			.headers((headers) -> headers.setBearerAuth(accessToken))
			.retrieve()
			.onStatus((status) -> true, (request, response) -> {
			})
			.toEntity(String.class);
	}
```

Das Beispiel hat kein Refresh Token: Läuft das Access Token ab, meldet sich der Benutzer erneut an.

## 6. Die App ausführen

Starten Sie die API aus dem Express-Schnellstart (oder eine andere API, die die Tokens akzeptiert) und führen Sie die App mit exportierten Variablen aus:

```sh
mvn spring-boot:run
```

Öffnen Sie `http://localhost:8080`, wählen Sie **Anmelden**, melden Sie sich bei KeenAuth an und öffnen Sie das Dashboard.

## 7. Ohne Netzwerk testen

Die Tests benötigen weder Netzwerk noch ein KeenAuth-Konto. Ein kleiner HTTP-Server auf einem Loopback-Port übernimmt die Rolle Ihrer Umgebung (Discovery, ein für den Testlauf erzeugter Schlüsselsatz, der Token-Endpunkt und Userinfo) sowie die der API. Der Test ersetzt die Client-Registrierung durch eine, die von diesem Server aus Discovery bezieht, und zwar über dieselbe Methode `registration`:

```java
	/** The registration of SecurityConfig, discovered from the fake environment instead of the issuer above. */
	@TestConfiguration
	static class FakeDiscovery {

		@Bean
		ClientRegistrationRepository clientRegistrationRepository(KeenAuthSettings settings) {
			return new InMemoryClientRegistrationRepository(
					SecurityConfig.registration(ClientRegistrations.fromIssuerLocation(KEENAUTH.issuer()), settings));
		}

	}
```

```java
	@Test
	public void callbackWithAnUnknownStateIsRefusedBeforeAnyTokenRequest() throws Exception {
		MockHttpSession session = new MockHttpSession();
		startSignIn(session);
		MvcResult result = returnFromKeenAuth(session, "not-the-state");
		assertEquals(result.getResponse().getRedirectedUrl(), "/login?error");
		assertTrue(KEENAUTH.requestsTo("/oauth2/token").isEmpty());
```

```sh
mvn dependency:list -DoutputFile=target/dependencies.txt
python3 ../scripts/maven_deps.py target/dependencies.txt    # SHA-256 lock and licences
mvn test
```

Die Tests prüfen, dass die Konfiguration beim Start validiert wird (und dass keine Fehlermeldung ein Geheimnis enthält); dass die Anmeldung PKCE S256, `state`, `nonce` und `resource` sendet; dass der Code mit dem passenden Verifier und mit Client-Authentifizierung eingetauscht wird; dass das Access Token im Header `Authorization` an die API geht und nie in ein Cookie gelangt; dass ein unbekannter State, ein ID Token für einen anderen Client und ein ID Token mit einem anderen Nonce ohne Sitzung abgelehnt werden; dass eine Seite ohne Sitzung die Anmeldung startet; und dass die Abmeldung das CSRF-Token benötigt und mit `id_token_hint` zum Logout-Endpunkt führt.

Die vollständige Anmeldung mit einem echten Browser benötigt eine KeenAuth-Umgebung. Sie ist nicht Teil der Offline-Tests.

## Hinter einem Proxy oder Load Balancer

Setzen Sie `APP_BASE_URL` auf die öffentliche URL der App; der Redirect-URI wird daraus gebildet. Wenn ein Proxy TLS beendet, setzen Sie `server.forward-headers-strategy=framework` nur, wenn der Proxy der einzige Weg zur App ist.

## Aktuelle Grenzen

- Die Anmeldung startet mit einem `GET` auf `/oauth2/authorization/keenauth`, wie es Spring Security standardmäßig tut. Die Prüfung von `state` und PKCE schützen den Callback.
- Spring Security prüft den Autorisierungs-Antwortparameter `iss` nicht (RFC 9207).
- Spring Security akzeptiert ID Tokens mit RS256 standardmäßig. Signiert Ihre Umgebung mit einem anderen Algorithmus, konfigurieren Sie eine Bean `OidcIdTokenDecoderFactory` mit diesem Algorithmus.
- Kein Refresh Token und keine stille Anmeldung.

## Nächste Schritte

- [Eine Express-API schützen](/docs/quickstarts/express): die API, die diese App aufruft.
- [Tokens prüfen](/docs/sdk/token-verification): der Verifier unter der Middleware Ihrer API.
