Last active
March 3, 2023 09:48
-
-
Save bastman/67e4cdb52127f30fba8185bf08d71719 to your computer and use it in GitHub Desktop.
kotlin-spring-security-auth0-api-utils: extensions for auth0 (e.g. handle custom claims)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.auth0utils | |
import com.auth0.jwk.JwkProviderBuilder | |
import com.auth0.jwt.JWT | |
import com.auth0.jwt.JWTVerifier | |
import com.auth0.jwt.exceptions.JWTDecodeException | |
import com.auth0.jwt.exceptions.JWTVerificationException | |
import com.auth0.jwt.interfaces.DecodedJWT | |
import com.auth0.spring.security.api.JwtAuthenticationEntryPoint | |
import com.auth0.spring.security.api.JwtAuthenticationProvider | |
import com.auth0.spring.security.api.authentication.JwtAuthentication | |
import org.apache.commons.codec.binary.Base64 | |
import org.slf4j.LoggerFactory | |
import org.springframework.security.authentication.AuthenticationProvider | |
import org.springframework.security.config.annotation.web.builders.HttpSecurity | |
import org.springframework.security.config.http.SessionCreationPolicy | |
import org.springframework.security.core.Authentication | |
import org.springframework.security.core.GrantedAuthority | |
import org.springframework.security.core.authority.SimpleGrantedAuthority | |
import org.springframework.security.core.context.SecurityContext | |
import org.springframework.security.core.context.SecurityContextHolder | |
import org.springframework.security.web.context.HttpRequestResponseHolder | |
import org.springframework.security.web.context.SecurityContextRepository | |
import java.time.Instant | |
import javax.servlet.http.HttpServletRequest | |
import javax.servlet.http.HttpServletResponse | |
import com.auth0.spring.security.api.BearerSecurityContextRepository as Auth0BearerSecurityContextRepository | |
typealias JwtAuthoritiesConverter = (DecodedJWT) -> List<String> | |
class CustomJwtWebSecurityConfigurer( | |
private val audience: String, | |
private val issuer: String, | |
private val provider: AuthenticationProvider, | |
private val authoritiesConverter: JwtAuthoritiesConverter | |
) { | |
@Throws(Exception::class) | |
fun configure(http: HttpSecurity): HttpSecurity = | |
http | |
.authenticationProvider(provider) | |
.securityContext() | |
.securityContextRepository( | |
CustomBearerSecurityContextRepository(authoritiesConverter = authoritiesConverter) | |
) | |
.and() | |
.exceptionHandling() | |
.authenticationEntryPoint(JwtAuthenticationEntryPoint()) | |
.and() | |
.httpBasic().disable() | |
.csrf().disable() | |
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() | |
companion object { | |
val AUTHORITIES_CONVERTER_AUTH0_DEFAULT: JwtAuthoritiesConverter = { it.scopes(claimName = "scope") } | |
fun forRS256( | |
audience: String, issuer: String, provider: AuthenticationProvider? = null, | |
authoritiesConverter: JwtAuthoritiesConverter = AUTHORITIES_CONVERTER_AUTH0_DEFAULT | |
): CustomJwtWebSecurityConfigurer { | |
val authProvider = if (provider == null) { | |
val jwkProvider = JwkProviderBuilder(issuer).build() | |
JwtAuthenticationProvider(jwkProvider, issuer, audience) | |
} else { | |
provider | |
} | |
return CustomJwtWebSecurityConfigurer(audience, issuer, authProvider, authoritiesConverter) | |
} | |
fun forHS256WithBase64Secret( | |
audience: String, issuer: String, secret: String, | |
authoritiesConverter: JwtAuthoritiesConverter = AUTHORITIES_CONVERTER_AUTH0_DEFAULT | |
): CustomJwtWebSecurityConfigurer { | |
val secretBytes = Base64(true).decode(secret) | |
return CustomJwtWebSecurityConfigurer( | |
audience = audience, issuer = issuer, | |
provider = JwtAuthenticationProvider(secretBytes, issuer, audience), | |
authoritiesConverter = authoritiesConverter | |
) | |
} | |
fun forHS256( | |
audience: String, issuer: String, secret: ByteArray, | |
authoritiesConverter: JwtAuthoritiesConverter = AUTHORITIES_CONVERTER_AUTH0_DEFAULT | |
): CustomJwtWebSecurityConfigurer = | |
CustomJwtWebSecurityConfigurer( | |
audience = audience, issuer = issuer, | |
provider = JwtAuthenticationProvider(secret, issuer, audience), | |
authoritiesConverter = authoritiesConverter | |
) | |
fun forHS256( | |
audience: String, issuer: String, provider: AuthenticationProvider, | |
authoritiesConverter: JwtAuthoritiesConverter = AUTHORITIES_CONVERTER_AUTH0_DEFAULT | |
): CustomJwtWebSecurityConfigurer = | |
CustomJwtWebSecurityConfigurer(audience, issuer, provider, authoritiesConverter) | |
} | |
} | |
class CustomBearerSecurityContextRepository( | |
private val authoritiesConverter: JwtAuthoritiesConverter | |
) : SecurityContextRepository { | |
override fun saveContext(context: SecurityContext, request: HttpServletRequest, response: HttpServletResponse) {} | |
override fun containsContext(request: HttpServletRequest): Boolean = tokenFromRequest(request) != null | |
override fun loadContext(requestResponseHolder: HttpRequestResponseHolder): SecurityContext { | |
val context = SecurityContextHolder.createEmptyContext() | |
val token = tokenFromRequest(requestResponseHolder.request) | |
val authentication = CustomPreAuthenticatedAuthenticationJsonWebToken | |
.usingToken(token = token, authoritiesConverter = authoritiesConverter) | |
if (authentication != null) { | |
context.authentication = authentication | |
logger.debug("Found bearer token in request. Saving it in SecurityContext") | |
} | |
return context | |
} | |
private fun tokenFromRequest(request: HttpServletRequest): String? { | |
val value = request.getHeader("Authorization") | |
if (value == null || !value.toLowerCase().startsWith("bearer")) { | |
return null | |
} | |
val parts = value.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() | |
return if (parts.size < 2) { | |
null | |
} else parts[1].trim { it <= ' ' } | |
} | |
companion object { | |
private val logger = LoggerFactory.getLogger(this::class.java); | |
} | |
} | |
class CustomAuthenticationJsonWebToken @Throws(JWTVerificationException::class) | |
constructor( | |
token: String, | |
verifier: JWTVerifier?, | |
private val authoritiesConverter: JwtAuthoritiesConverter | |
) : Authentication, JwtAuthentication { | |
private val decoded: DecodedJWT = if (verifier == null) JWT.decode(token) else verifier.verify(token) | |
private var authenticated: Boolean = verifier != null | |
override fun getToken(): String = decoded.token | |
override fun getKeyId(): String = decoded.keyId | |
override fun getCredentials(): Any = decoded.token | |
override fun getDetails(): DecodedJWT = decoded | |
override fun getPrincipal(): Any = decoded.subject | |
override fun isAuthenticated(): Boolean = authenticated | |
override fun getName(): String = decoded.subject | |
@Throws(JWTVerificationException::class) | |
override fun verify(verifier: JWTVerifier): Authentication = | |
CustomAuthenticationJsonWebToken(token, verifier, authoritiesConverter) | |
@Throws(IllegalArgumentException::class) | |
override fun setAuthenticated(isAuthenticated: Boolean) { | |
if (isAuthenticated) { | |
throw IllegalArgumentException("Must create a new instance to specify that the authentication is valid") | |
} | |
this.authenticated = false | |
} | |
override fun getAuthorities(): Collection<GrantedAuthority> = | |
authoritiesConverter.invoke(decoded) | |
.map { SimpleGrantedAuthority(it) } | |
.toMutableList() | |
} | |
class CustomPreAuthenticatedAuthenticationJsonWebToken( | |
private val token: DecodedJWT, | |
private val authoritiesConverter: JwtAuthoritiesConverter | |
) : Authentication, JwtAuthentication { | |
override fun getAuthorities(): Collection<GrantedAuthority> = emptyList() | |
override fun getCredentials(): Any = token.token | |
override fun getDetails(): Any = token | |
override fun getPrincipal(): Any = token.subject | |
override fun isAuthenticated(): Boolean = false | |
@Throws(IllegalArgumentException::class) | |
override fun setAuthenticated(isAuthenticated: Boolean) { | |
} | |
override fun getName(): String = token.subject | |
override fun getToken(): String = token.token | |
override fun getKeyId(): String = token.keyId | |
@Throws(JWTVerificationException::class) | |
override fun verify(verifier: JWTVerifier): Authentication = | |
CustomAuthenticationJsonWebToken( | |
token = token.token, | |
verifier = verifier, | |
authoritiesConverter = authoritiesConverter | |
) | |
companion object { | |
private val logger = LoggerFactory.getLogger(this::class.java) | |
fun usingToken( | |
token: String?, | |
authoritiesConverter: JwtAuthoritiesConverter | |
): CustomPreAuthenticatedAuthenticationJsonWebToken? = | |
if (token == null) { | |
logger.debug("No token was provided to build ${this::class.java.name}") | |
null | |
} else { | |
try { | |
CustomPreAuthenticatedAuthenticationJsonWebToken( | |
token = JWT.decode(token), | |
authoritiesConverter = authoritiesConverter | |
) | |
} catch (e: JWTDecodeException) { | |
logger.debug("Failed to decode token as jwt", e) | |
null | |
} | |
} | |
} | |
} | |
fun DecodedJWT.claimAsString(name: String): String? { | |
val claim = this.getClaim(name) | |
return if (claim == null || claim.isNull()) { | |
null | |
} else claim.asString() | |
} | |
fun DecodedJWT.expiresAt(): Instant? = this.expiresAt?.toInstant() | |
fun DecodedJWT.principal(): String = this.subject ?: "" | |
fun DecodedJWT.credentials(): String = this.token ?: "" | |
fun DecodedJWT.claimAsListOfString(claimName: String): List<String> { | |
val claim = this.getClaim(claimName) | |
return if (claim == null || claim.isNull) { | |
emptyList() | |
} else { | |
claim.asList(String::class.java) | |
.toList() | |
.map { it.trim() } | |
.filter { it.isNotEmpty() } | |
.distinct() | |
} | |
} | |
fun DecodedJWT.scopes(claimName: String = "scope"): List<String> { | |
val scope = claimAsString(claimName) | |
if (scope == null || scope.trim { it <= ' ' }.isEmpty()) { | |
return emptyList() | |
} | |
val scopes = scope.split(" ".toRegex()).dropLastWhile { it.isEmpty() } | |
return scopes.map { it.trim() }.filter { it.isNotEmpty() }.distinct() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.config | |
import com.example.auth0utils.JwtWebSecurityConfigurer | |
import com.example.auth0utils.claimAsListOfString | |
import com.auth0.jwt.interfaces.DecodedJWT | |
import org.springframework.beans.factory.annotation.Value | |
import org.springframework.http.HttpMethod | |
import org.springframework.security.config.annotation.web.builders.HttpSecurity | |
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter | |
import org.springframework.stereotype.Component | |
@Component | |
class Auth0WebSecurity( | |
@Value(value = "\${app.auth.auth0.apiAudience}") | |
private val apiAudience: String, | |
@Value(value = "\${app.auth.auth0.issuer}") | |
private val issuer: String | |
) : WebSecurityConfigurerAdapter() { | |
override fun configure(http: HttpSecurity) { | |
JwtWebSecurityConfigurer | |
.forRS256( | |
audience = apiAudience, | |
issuer = issuer, | |
provider = null | |
) { it: DecodedJWT -> | |
it.claimAsListOfString("https://example.com/claims/roles") | |
} | |
.configure(http) | |
.authorizeRequests() | |
.antMatchers(HttpMethod.GET, "/ping").authenticated() | |
.anyRequest().authenticated() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment