Zu Hause arbeiten: Vor- & Nachteile und unsere Erfahrungen damit
22. Februar 2023Retrospektiven: die Gegenwart verstehen und die Zukunft gestalten
18. April 2023JWE Tokens mit Spring Security entschlüsseln und verifizieren
Alle Systeme, die einer ständigen Flut von Anfragen ausgesetzt sind, müssen aufgrund der heutigen Anforderungen in kurzer Zeit antworten. Deshalb ist es wichtig, dass die einzelnen Schritte zur Verarbeitung einer Anfrage so schnell und sicher wie möglich erfolgen. Daher werden z. B. zum Zwecke der Authentifizierung zustandslose Protokolle wie OAuth 2.0 (oder auch OIDC) verwendet. Dieses Protokoll nutzt für gewöhnlich sogenannte JWTs (JSON Web Token). Diese Tokens enthalten Daten über den Anfragenden und dessen Rechte im System. Die Informationen sind für gewöhnlich für jeden lesbar, der Zugriff auf die Kommunikation hat und auch für alle, die an der Verbindung zwischen Nutzer und System beteiligt sind (z. B. Browser, Proxyserver oder Reverse Proxy).
Für gewöhnlich wird hier von JWS (JSON Web Signature) Tokens gesprochen. Damit eine potenzielle Veränderung während der Übertragung festgestellt werden kann, sind diese Tokens im Normalfall mittels Signaturen dagegen gesichert (Unveränderbarkeit). Als Ergänzung dazu gibt es eine weitere Form der JWTs, die JWE (JSON Web Encryption) Tokens, welche den Inhalt verschlüsselt übertragen (Vertraulichkeit). Die Verschlüsselung stellt dabei nicht sicher, wer die Daten erstellt hat. Daher werden bei der Nutzung von JWE Tokens im Normalfall ein JWS Token als Inhalt verwendet, um die notwendige Sicherheit zu gewährleisten.
Was sind JWE Tokens in Kürze?
JWE Tokens sind im Grunde eine standardisierte Möglichkeit, strukturierte Daten in verschlüsselter Form zwischen Systemen auszutauschen. Für die Verschlüsselung werden vom Standard verschiedenste symmetrische oder asymmetrische Verfahren angeboten. Welche es genau gibt, kann man im zugehörigen Standard RFC-7518 nachlesen. Der zu übertragende Payload enthält zur stärkeren Sicherheit ein JWS Token, welches die Unveränderbarkeit und Korrektheit der Daten sicherstellt.
Wie sind JWS und JWE Tokens aufgebaut?
Ein JWS Token besteht aus drei Teilen, die durch einen Punkt (.
) getrennt sind – {Header}.{Payload}.{Signatur}
. Der Header
enthält unter anderem Informationen darüber, mit welchem Algorithmus die Signatur erstellt wurde und diese demnach verifiziert werden kann. Der Payload
enthält die zu übertragenden Daten und letzte Teil enthält die digitale Signatur.
Ein JWE Token besteht aus fünf Teilen, die durch einen Punkt (.
) getrennt sind – {Header}.{EncryptionKey}.{InitializationVector}.{Ciphertext}.{AuthenticationTag}
. Der Header
enthält unter anderem Informationen darüber, mit welchem Algorithmus die Daten verschlüsselt wurden und ob sich in den Daten ein JWS befindet. Die zwei Teile EncryptionKey
und InitializationVector
enthalten technische Daten, die für die Entschlüsselung der Daten relevant sind. Der Ciphertext
enthält die verschlüsselten Daten. Der AuthenticationTag wird verwendet, um sicherzustellen, dass das Token nicht verändert wurde.
Wer mehr zum Thema JWS und JWE Tokens erfahren möchte, kann sich z. B. einen Deep Dive zum Thema (auf Englisch) durchlesen oder in die jeweiligen Spezifikationen schauen.
Wie können JWE Tokens mit Spring Boot bzw. Spring Security entschlüsselt und verifiziert werden?
Spring Boot in Kombination mit Spring Security enthält die Funktionalität, die Prüfung von JWS und JWE Tokens sehr einfach in den Authentifizierungsprozess des Systems einzubinden.
Das Einbinden folgender Dependency (z.B. via Maven) erweitert das System um diese Funktion:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency>
Wen es interessiert: Die Konfiguration im Spring Boot Framework werden an folgenden Stellen konkret anhand der nachfolgenden Werte durchgeführt
- org.springframework.boot.autoconfigure.security.oauth2.resource.servlet. OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration: jwtDecoderByJwkKeySetUri
- org.springframework.boot.autoconfigure.security.oauth2.resource.servlet. OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChain Configuration
Einbinden der JWS Token Prüfung in Spring Boot
Die folgende Spring Boot Konfiguration definiert, wie die Tokens verifiziert werden können
JwsSecurityConfiguration.java @Configuration @EnableWebSecurity public class JwsSecurityConfiguration {}
application.yaml spring: security: oauth2: resourceserver: jwt: jwk-set-uri: ${url}/.well-known/jwks.json issuer-uri: ${url} audiences: resource-server-app
Einbinden der JWE Token Prüfung in Spring Boot
Um die Prüfung von JWE Tokens einzubinden, kann folgende Konfiguration verwendet werden. Dazu wird u. a. der private Schlüssel für die Entschlüsselung benötigt, welcher in diesem Beispiel ein RSA Private Key ist. Dazu muss definiert werden, für welche Algorithmen der zur Verfügung gestellte private Schlüssel zur Entschlüsselung verwendet werden kann. Diese Konfiguration basiert in großen Teilen auf einem Beispielprojekt des Spring Security Frameworks, welches hier abgerufen werden kann.
JwtDecoderConfiguration.java @Configuration @EnableWebSecurity public class JwtDecoderConfiguration { private final JWEAlgorithm jweAlgorithm = JWEAlgorithm.RSA_OAEP_256; private final EncryptionMethod encryptionMethod = EncryptionMethod.A256GCM; private final OAuth2ResourceServerProperties.Jwt properties; private final RSAPrivateKey key; JwtDecoderConfiguration(OAuth2ResourceServerProperties properties, @Value("${sample.jwe-key-value}") RSAPrivateKey key) { this.properties = properties.getJwt(); this.key = key; } @Bean JwtDecoder jwtDecoderByJwkKeySetUri() { NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) .jwsAlgorithms(this::jwsAlgorithms) .jwtProcessorCustomizer(this::jwtProcessorCustomizer) .build(); String issuerUri = this.properties.getIssuerUri(); OAuth2TokenValidator<Jwt> defaultValidator = JwtValidators.createDefaultWithIssuer(issuerUri); nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator)); return nimbusJwtDecoder; } private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) { for (String algorithm : this.properties.getJwsAlgorithms()) { signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); } } private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) { List<String> audiences = this.properties.getAudiences(); List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>(); validators.add(defaultValidator); validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD, (aud) -> aud != null && !Collections.disjoint(aud, audiences))); return new DelegatingOAuth2TokenValidator<>(validators); } private void jwtProcessorCustomizer(ConfigurableJWTProcessor<SecurityContext> jwtProcessor) { JWKSource<SecurityContext> jweJwkSource = new ImmutableJWKSet<>(new JWKSet(rsaKey())); JWEKeySelector<SecurityContext> jweKeySelector = new JWEDecryptionKeySelector<>(this.jweAlgorithm, this.encryptionMethod, jweJwkSource); jwtProcessor.setJWEKeySelector(jweKeySelector); } private RSAKey rsaKey() { RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key; Base64URL n = Base64URL.encode(crtKey.getModulus()); Base64URL e = Base64URL.encode(crtKey.getPublicExponent()); return new RSAKey.Builder(n, e) .privateKey(this.key) .keyUse(KeyUse.ENCRYPTION) .build(); } }
application.yaml spring: security: oauth2: resourceserver: jwt: jwk-set-uri: ${url}/.well-known/jwks.json issuer-uri: ${url} audiences: resource-server-app sample: jwe-key-value: classpath:private.key
private.key -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----
Testen der Authentifizierung mit JWE Tokens
Damit die Konfiguration für die Authentifizierung getestet werden kann, müssen in den Tests valide Tokens erstellt werden. Diese JWE Tokens können zum Beispiel mit folgender Klasse erzeugt werden. Die Inhalte usw. müssen natürlich auf die jeweiligen benötigen Anforderungen angepasst werden.
@Component public class TokenService { private final JWEAlgorithm jweAlgorithm = RSA_OAEP_256; private final EncryptionMethod encryptionMethod = A256GCM; private final JWSAlgorithm jwsAlgorithm = RS256; private final OAuth2ResourceServerProperties.Jwt properties; private final RSAPrivateKey key; TokenService(OAuth2ResourceServerProperties properties, @Value("${sample.jwe-key-value}") RSAPrivateKey key) { this.properties = properties.getJwt(); this.key = key; } public String buildToken() throws Exception { SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(jwsAlgorithm).build(), new JWTClaimsSet.Builder() .issuer(properties.getIssuerUri()) .audience(properties.getAudiences()) .subject("subject") .issueTime(new Date()) .expirationTime(new Date(new Date().getTime() + 5000)) .build()); signedJWT.sign(new RSASSASigner(rsaSigningKey())); JWEObject jweObject = new JWEObject( new JWEHeader.Builder(jweAlgorithm, encryptionMethod).contentType("JWT").build(), new Payload(signedJWT)); RSAEncrypter encrypter = new RSAEncrypter(rsaEncryptionKey()); jweObject.encrypt(encrypter); return jweObject.serialize(); } private RSAKey rsaEncryptionKey() { RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key; Base64URL n = Base64URL.encode(crtKey.getModulus()); Base64URL e = Base64URL.encode(crtKey.getPublicExponent()); return new RSAKey.Builder(n, e).keyUse(KeyUse.ENCRYPTION).build(); } private RSAKey rsaSigningKey() { RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key; Base64URL n = Base64URL.encode(crtKey.getModulus()); Base64URL e = Base64URL.encode(crtKey.getPublicExponent()); return new RSAKey.Builder(n, e) .privateKey(crtKey) .keyUse(KeyUse.SIGNATURE) .build(); } }
Fazit
JWE Tokens werden unserer Erfahrung nach nicht oft eingesetzt. Daher sind Informationen bzgl. der Verwendung in Frameworks wie Spring Boot und Spring Security rar gesät. Mit diesem Blogbeitrag möchten wir anderen Entwicklern, die mit JWE Tokens und Spring Boot arbeiten müssen, evtl. die Arbeit etwas erleichtern. Viel Spaß damit