Skip to content

Instantly share code, notes, and snippets.

@alexaugustobr
Forked from bastman/auth0.kt
Created October 14, 2018 15:51
Show Gist options
  • Save alexaugustobr/2e817116363502a873c28405b1daec7b to your computer and use it in GitHub Desktop.
Save alexaugustobr/2e817116363502a873c28405b1daec7b to your computer and use it in GitHub Desktop.
kotlin-spring-security-auth0-api-utils: extensions for auth0 (e.g. handle custom claims)
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()
}
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