У нас есть приложение, которое использует пружинный отдых безопасности в качестве механизма безопасности, и мы решили переключиться на Auth0.
Я пытаюсь заставить плагин Grails Spring Security Rest работать с токенами Auth0.
Исходя из документации, кажется, что самый простой способ достичь этого - реализовать собственное хранилище токенов, которое будет проверять токен на соответствие Auth0
.
Прямо сейчас я просто создал свой собственный класс хранения токенов, чтобы понять, как именно это работает.
Моя реализация хранилища безопасности в настоящее время точно такая же, как и в исходном JwtTokenStorage
(пока я пытаюсь понять механизм):
package priz.be
import com.nimbusds.jose.JOSEException
import com.nimbusds.jwt.JWT
import grails.plugin.springsecurity.rest.JwtService
import grails.plugin.springsecurity.rest.token.storage.TokenNotFoundException
import grails.plugin.springsecurity.rest.token.storage.TokenStorageService
import grails.plugin.springsecurity.rest.token.storage.jwt.JwtTokenStorageService
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import java.text.ParseException
@Slf4j
@CompileStatic
class Auth0TokenStorageService extends JwtTokenStorageService {
JwtService jwtService
UserDetailsService userDetailsService
@Override
UserDetails loadUserByToken(String tokenValue) throws TokenNotFoundException {
Date now = new Date()
try {
JWT jwt = jwtService.parse(tokenValue)
if (jwt.JWTClaimsSet.expirationTime?.before(now)) {
throw new TokenNotFoundException("Token ${tokenValue} has expired")
}
boolean isRefreshToken = jwt.JWTClaimsSet.expirationTime == null
if (isRefreshToken) {
UserDetails principal = userDetailsService.loadUserByUsername(jwt.JWTClaimsSet.subject)
if (!principal) {
throw new TokenNotFoundException("Token no longer valid, principal not found")
}
if (!principal.enabled) {
throw new TokenNotFoundException("Token no longer valid, account disabled")
}
if (!principal.accountNonExpired) {
throw new TokenNotFoundException("Token no longer valid, account expired")
}
if (!principal.accountNonLocked) {
throw new TokenNotFoundException("Token no longer valid, account locked")
}
if (!principal.credentialsNonExpired) {
throw new TokenNotFoundException("Token no longer valid, credentials expired")
}
return principal
}
def roles = jwt.JWTClaimsSet.getStringArrayClaim('roles')?.collect { String role -> new SimpleGrantedAuthority(role) }
log.debug "Successfully verified JWT"
log.debug "Trying to deserialize the principal object"
try {
UserDetails details = JwtService.deserialize(jwt.JWTClaimsSet.getStringClaim('principal'))
log.debug "UserDetails deserialized: ${details}"
if (details) {
return details
}
} catch (exception) {
log.debug(exception.message)
}
log.debug "Returning a org.springframework.security.core.userdetails.User instance"
return new User(jwt.JWTClaimsSet.subject, 'N/A', roles)
} catch (ParseException pe) {
throw new TokenNotFoundException("Token ${tokenValue} is not valid")
} catch (JOSEException je) {
throw new TokenNotFoundException("Token ${tokenValue} has an invalid signature")
}
}
@Override
void storeToken(String tokenValue, UserDetails principal) {
log.debug "Nothing to store as this is a stateless implementation"
}
@Override
void removeToken(String tokenValue) throws TokenNotFoundException {
log.debug "Nothing to remove as this is a stateless implementation"
throw new TokenNotFoundException("Token ${tokenValue} cannot be removed as this is a stateless implementation")
}
}
Если я отправляю запрос в API с действительным токеном Auth0, я получаю в качестве ответа недопустимый токен.
Копая немного глубже, я обнаружил, что, пока код пытается проверить токен, он переходит в функцию JwtService.parse
, где он разрешает токен как HMAC signed
(не уверен почему, но давайте предположим, что для момент правильный) здесь:
JWT parse(String tokenValue) {
JWT jwt = JWTParser.parse(tokenValue)
if (jwt instanceof SignedJWT) {
log.debug "Parsed an HMAC signed JWT"
SignedJWT signedJwt = jwt as SignedJWT
if(!signedJwt.verify(new MACVerifier(jwtSecret))) {
throw new JOSEException('Invalid signature')
}
} else if (jwt instanceof EncryptedJWT) {
...
return jwt
}
Затем он пытается делегировать проверку на MACVerifier
, однако при разрешении имени алгоритма происходит сбой, поскольку RS256 не поддерживается:
protected static String getJCAAlgorithmName(final JWSAlgorithm alg)
throws JOSEException {
if (alg.equals(JWSAlgorithm.HS256)) {
return "HMACSHA256";
} else if (alg.equals(JWSAlgorithm.HS384)) {
return "HMACSHA384";
} else if (alg.equals(JWSAlgorithm.HS512)) {
return "HMACSHA512";
} else {
throw new JOSEException(AlgorithmSupportMessage.unsupportedJWSAlgorithm(
alg,
SUPPORTED_ALGORITHMS));
}
}
Я совершенно уверен, что это то, что я делаю неправильно, просто могу придумать что-нибудь на этом этапе.
Что мне здесь не хватает?