diff --git a/demo/src/main/kotlin/Server.kt b/demo/src/main/kotlin/Server.kt index 99338ae5..2e418bfe 100644 --- a/demo/src/main/kotlin/Server.kt +++ b/demo/src/main/kotlin/Server.kt @@ -64,6 +64,7 @@ object Server : ServerPathGroup(ServerPath.root) { val database = setting("database", DatabaseSettings()) val email = setting("email", EmailSettings()) + val jwtSigner = setting("jwt", JwtSigner()) val sms = setting("sms", SMSSettings()) val files = setting("files", FilesSettings()) val cache = setting("cache", CacheSettings()) @@ -137,7 +138,7 @@ object Server : ServerPathGroup(ServerPath.root) { val emailAccess = userInfo.userEmailAccess { User(email = it) } val passAccess = userInfo.userPasswordAccess { username, hashed -> User(email = username, hashedPassword = hashed) } - val baseAuth = BaseAuthEndpoints(path, emailAccess, expiration = 365.days, emailExpiration = 1.hours) + val baseAuth = BaseAuthEndpoints(path, emailAccess, jwtSigner) val emailAuth = EmailAuthEndpoints(baseAuth, emailAccess, cache, email) val passAuth = PasswordAuthEndpoints(baseAuth, passAccess) } diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/BaseAuthEndpoints.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/BaseAuthEndpoints.kt index ed3d6091..d68941cd 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/BaseAuthEndpoints.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/BaseAuthEndpoints.kt @@ -35,9 +35,11 @@ import kotlin.time.Duration.Companion.minutes * @param landing The landing page for after a user is authenticated. Defaults to the root. * @param handleToken The action to perform upon obtaining the token. Defaults to redirecting to [landing], but respects paths given in the `destination` query parameter. */ +@Deprecated("Move to new auth") open class BaseAuthEndpoints, ID : Comparable>( path: ServerPath, val userAccess: UserAccess, + val jwtSigner: () -> JwtSigner, val expiration: Duration = 365.days, val emailExpiration: Duration = 30.minutes, val landing: String = "/", @@ -51,7 +53,6 @@ open class BaseAuthEndpoints, ID : Comparable>( } ) }, - val hasher: () -> SecureHasher = secretBasis.hasher("old-auth"), ) : ServerPathGroup(path) { val typeName = userAccess.authType.classifier?.toString()?.substringAfterLast('.') ?: "Unknown" @@ -93,7 +94,7 @@ open class BaseAuthEndpoints, ID : Comparable>( request.headers[HttpHeader.Authorization] ?: request.headers.cookies[HttpHeader.Authorization] ?: return null token = token.removePrefix("Bearer ") - val claims = hasher().verifyJwt(token) ?: return null + val claims = jwtSigner().hasher.verifyJwt(token) ?: return null val sub = claims.sub ?: return null if (!sub.startsWith(jwtPrefix)) return null val id = @@ -118,8 +119,8 @@ open class BaseAuthEndpoints, ID : Comparable>( /** * Creates a JWT representing the given [user]. */ - suspend fun refreshToken(token: String, expireDuration: Duration = expiration): String = hasher().signJwt( - hasher().verifyJwt(token)!!.copy( + suspend fun refreshToken(token: String, expireDuration: Duration = expiration): String = jwtSigner().hasher.signJwt( + jwtSigner().hasher.verifyJwt(token)!!.copy( exp = now().plus(expireDuration).epochSeconds, ) ) @@ -133,7 +134,7 @@ open class BaseAuthEndpoints, ID : Comparable>( /** * Creates a JWT representing the given user by [id]. */ - suspend fun tokenById(id: ID, expireDuration: Duration = expiration): String = hasher().signJwt( + suspend fun tokenById(id: ID, expireDuration: Duration = expiration): String = jwtSigner().hasher.signJwt( JwtClaims( iss = generalSettings().publicUrl, aud = generalSettings().publicUrl, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/EmailAuthEndpoints.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/EmailAuthEndpoints.kt index 040293e3..cd628e3e 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/EmailAuthEndpoints.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/EmailAuthEndpoints.kt @@ -29,6 +29,7 @@ import kotlin.time.Duration.Companion.minutes * Also allows for OAuth based login, as most OAuth systems share email as a common identifier. * For information on setting up OAuth, see the respective classes, [OauthAppleEndpoints], [OauthGitHubEndpoints], [OauthGoogleEndpoints], [OauthMicrosoftEndpoints]. */ +@Deprecated("Move to new auth") open class EmailAuthEndpoints, ID: Comparable>( val base: BaseAuthEndpoints, val emailAccess: UserEmailAccess, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/JwtSigner.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/JwtSigner.kt new file mode 100644 index 00000000..b409fb0e --- /dev/null +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/JwtSigner.kt @@ -0,0 +1,159 @@ +@file:UseContextualSerialization(Duration::class) + +package com.lightningkite.lightningserver.auth + +import com.lightningkite.lightningserver.encryption.* +import com.lightningkite.lightningserver.encryption.SecureHasher.* +import com.lightningkite.lightningserver.exceptions.UnauthorizedException +import com.lightningkite.lightningserver.serialization.Serialization +import com.lightningkite.lightningserver.serialization.decodeUnwrappingString +import com.lightningkite.lightningserver.serialization.encodeUnwrappingString +import com.lightningkite.lightningserver.settings.generalSettings +import com.lightningkite.now +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.getContextualDescriptor +import java.security.SecureRandom +import java.util.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds + + +private val availableCharacters = + "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM~!@#%^&*()_+`-=[]{};':,./<>?" + +/** + * AuthSettings holds the values required to setup JWT Authentication. + * This will be used by nearly every function in the auth package. + * @param expirationMilliseconds The default expiration for tokens. This can be overridden for a specific token. + * @param secret THis should be a long and complicated String. The jwtSecret should never be shared since it is what's used to sign JWTs. + */ +@Deprecated("Move to new auth") +@Serializable +data class JwtSigner( + val expiration: Duration = 365.days, + val emailExpiration: Duration = 1.hours, + val secret: String = buildString { + val rand = SecureRandom.getInstanceStrong() + repeat(64) { + append( + availableCharacters[rand.nextInt(availableCharacters.length)] + ) + } + }, + val issuer: String? = null, + val audience: String? = null +) { + + @kotlinx.serialization.Transient + val hasher = HS256(secret.toByteArray()) + + /** + * @return A JWT with the [subject], expiring in [expireDuration]. + */ + fun token(subject: String, expireDuration: Duration = expiration): String { + return hasher.signJwt(JwtClaims( + iss = issuer ?: generalSettings().publicUrl, + aud = audience ?: generalSettings().publicUrl, + exp = (now() + expireDuration).epochSeconds, + sub = subject, + iat = now().epochSeconds, + scope = "*" + )) + } + + /** + * Returns the subject if the token was valid. + */ + fun verify(token: String): String { + return try { + val claims = hasher.verifyJwt(token, audience) + claims!!.sub!! + } catch (e: JwtExpiredException) { + throw UnauthorizedException( + message = "This authorization has expired.", + cause = e + ) + } catch (e: JwtException) { + throw UnauthorizedException( + message = "Invalid token", + cause = e + ) + } catch (e: Exception) { + throw UnauthorizedException( + message = "Invalid token", + cause = e + ) + } + } + + + @Deprecated( + "Use the version with duration instead", + ReplaceWith("token(subject, Duration.ofMillis(expireDuration))", "java.time.Duration") + ) + inline fun token(subject: T, expireDuration: Long): String = + token(Serialization.module.serializer(), subject, expireDuration.milliseconds) + + @Deprecated( + "Use the version with duration instead", + ReplaceWith("token(serializer, subject, Duration.ofMillis(expireDuration))", "java.time.Duration") + ) + fun token(serializer: KSerializer, subject: T, expireDuration: Long): String = + token(serializer, subject, expireDuration.milliseconds) + + inline fun token(subject: T, expireDuration: Duration = expiration): String = + token(Serialization.module.serializer(), subject, expireDuration) + + fun token(serializer: KSerializer, subject: T, expireDuration: Duration = expiration): String { + return hasher.signJwt(JwtClaims( + iss = issuer ?: generalSettings().publicUrl, + aud = audience ?: generalSettings().publicUrl, + exp = (now() + expireDuration).epochSeconds, + sub = Serialization.json.encodeUnwrappingString(serializer, subject), + iat = now().epochSeconds, + scope = "*" + )) + } + + inline fun verify(token: String): T = verify(Serialization.module.serializer(), token) + fun verify(serializer: KSerializer, token: String): T { + return try { + hasher.verifyJwt(token, audience)!!.sub!!.let { + Serialization.json.decodeUnwrappingString(serializer, it) + } + } catch (e: JwtExpiredException) { + throw UnauthorizedException( + message = "This authorization has expired.", + cause = e + ) + } catch (e: JwtException) { + throw UnauthorizedException( + message = "Invalid token", + cause = e + ) + } catch (e: Exception) { + throw UnauthorizedException( + message = "Invalid token", + cause = e + ) + } + } + + private fun KSerializer<*>.isPrimitive(): Boolean { + var current = this.descriptor + while (true) { + when (current.kind) { + is PrimitiveKind -> return true + SerialKind.CONTEXTUAL -> current = + Serialization.json.serializersModule.getContextualDescriptor(current)!! + + else -> return false + } + } + } +} + diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PasswordAuthEndpoints.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PasswordAuthEndpoints.kt index b18baaa0..89ee23ab 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PasswordAuthEndpoints.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PasswordAuthEndpoints.kt @@ -16,6 +16,7 @@ import java.net.URLDecoder * Authentication via password. * Strongly not recommended. */ +@Deprecated("Move to new auth") open class PasswordAuthEndpoints, ID: Comparable>( val base: BaseAuthEndpoints, val info: UserPasswordAccess diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PinHandler.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PinHandler.kt index 156274f9..23b78634 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PinHandler.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/PinHandler.kt @@ -12,6 +12,7 @@ import java.security.SecureRandom import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes +@Deprecated("Move to new auth") open class PinHandler( private val cache: () -> Cache, val keyPrefix: String, diff --git a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/SmsAuthEndpoints.kt b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/SmsAuthEndpoints.kt index 055ef60b..c9449135 100644 --- a/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/SmsAuthEndpoints.kt +++ b/server-core/src/main/kotlin/com/lightningkite/lightningserver/auth/old/SmsAuthEndpoints.kt @@ -19,6 +19,7 @@ import kotlin.time.Duration.Companion.minutes /** * Authentication endpoints for logging in with SMS PINs. */ +@Deprecated("Move to new auth") open class SmsAuthEndpoints, ID: Comparable>( val base: BaseAuthEndpoints, val phoneAccess: UserPhoneAccess,