|
1 | 1 | package me.snoty.backend.server.plugins
|
2 | 2 |
|
3 |
| -import com.auth0.jwt.JWT |
4 |
| -import com.auth0.jwt.algorithms.Algorithm |
| 3 | +import com.auth0.jwk.UrlJwkProvider |
5 | 4 | import io.ktor.client.*
|
6 | 5 | import io.ktor.client.engine.apache.*
|
7 | 6 | import io.ktor.http.*
|
| 7 | +import io.ktor.http.auth.* |
8 | 8 | import io.ktor.server.application.*
|
9 | 9 | import io.ktor.server.auth.*
|
10 | 10 | import io.ktor.server.auth.jwt.*
|
11 | 11 | import io.ktor.server.response.*
|
12 | 12 | import io.ktor.server.routing.*
|
13 |
| -import io.ktor.server.sessions.* |
| 13 | +import kotlinx.serialization.Serializable |
| 14 | +import me.snoty.backend.config.Config |
| 15 | +import me.snoty.backend.server.handler.UnauthorizedException |
| 16 | +import me.snoty.backend.utils.respondStatus |
| 17 | +import java.net.URI |
14 | 18 |
|
15 |
| -fun Application.configureSecurity() { |
16 |
| - authentication { |
17 |
| - oauth("auth-oauth-google") { |
18 |
| - urlProvider = { "http://localhost:8080/callback" } |
| 19 | +fun Application.configureSecurity(config: Config) { |
| 20 | + val authConfig = config.authentication |
| 21 | + install(Authentication) { |
| 22 | + oauth("keycloak") { |
| 23 | + client = HttpClient(Apache) |
19 | 24 | providerLookup = {
|
20 | 25 | OAuthServerSettings.OAuth2ServerSettings(
|
21 |
| - name = "google", |
22 |
| - authorizeUrl = "https://accounts.google.com/o/oauth2/auth", |
23 |
| - accessTokenUrl = "https://accounts.google.com/o/oauth2/token", |
| 26 | + name = "keycloak", |
| 27 | + authorizeUrl = "${authConfig.oidcUrl}/auth", |
| 28 | + accessTokenUrl = "${authConfig.oidcUrl}/token", |
| 29 | + clientId = authConfig.clientId, |
| 30 | + clientSecret = authConfig.clientSecret, |
| 31 | + accessTokenRequiresBasicAuth = false, |
24 | 32 | requestMethod = HttpMethod.Post,
|
25 |
| - clientId = System.getenv("GOOGLE_CLIENT_ID"), |
26 |
| - clientSecret = System.getenv("GOOGLE_CLIENT_SECRET"), |
27 |
| - defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile") |
| 33 | + defaultScopes = listOf("openid") |
28 | 34 | )
|
29 | 35 | }
|
30 |
| - client = HttpClient(Apache) |
| 36 | + urlProvider = { |
| 37 | + "${config.publicHost}/callback" |
| 38 | + } |
31 | 39 | }
|
32 |
| - } |
33 |
| - // Please read the jwt property from the config file if you are using EngineMain |
34 |
| - val jwtAudience = "jwt-audience" |
35 |
| - val jwtDomain = "https://jwt-provider-domain/" |
36 |
| - val jwtRealm = "ktor sample app" |
37 |
| - val jwtSecret = "secret" |
38 |
| - authentication { |
39 |
| - jwt { |
40 |
| - realm = jwtRealm |
| 40 | + jwt("jwt-auth") { |
| 41 | + authHeader { call -> |
| 42 | + call.request.parseAuthorizationHeader() |
| 43 | + // optionally load from cookies |
| 44 | + ?: parseAuthorizationHeader("Bearer ${call.request.cookies["access_token"]}") |
| 45 | + } |
41 | 46 | verifier(
|
42 |
| - JWT |
43 |
| - .require(Algorithm.HMAC256(jwtSecret)) |
44 |
| - .withAudience(jwtAudience) |
45 |
| - .withIssuer(jwtDomain) |
46 |
| - .build() |
| 47 | + UrlJwkProvider(URI("${authConfig.oidcUrl}/certs").toURL()), |
| 48 | + authConfig.issuerUrl |
47 | 49 | )
|
| 50 | + challenge { _, _ -> |
| 51 | + call.respondStatus(UnauthorizedException("JWT is invalid")) |
| 52 | + } |
48 | 53 | validate { credential ->
|
49 |
| - if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null |
| 54 | + JWTPrincipal(credential.payload) |
50 | 55 | }
|
51 | 56 | }
|
52 | 57 | }
|
53 | 58 | routing {
|
54 |
| - authenticate("auth-oauth-google") { |
55 |
| - get("login") { |
56 |
| - call.respondRedirect("/callback") |
57 |
| - } |
58 |
| - |
| 59 | + authenticate("keycloak") { |
| 60 | + get("/login") {} |
59 | 61 | get("/callback") {
|
60 |
| - val principal: OAuthAccessTokenResponse.OAuth2? = call.authentication.principal() |
61 |
| - call.sessions.set(UserSession(principal?.accessToken.toString())) |
62 |
| - call.respondRedirect("/hello") |
| 62 | + val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>() |
| 63 | + ?: return@get call.respondRedirect("/login") |
| 64 | + |
| 65 | + call.response.cookies.append("access_token", principal.accessToken) |
| 66 | + call.respondText(principal.accessToken) |
| 67 | + } |
| 68 | + } |
| 69 | + authenticate("jwt-auth") { |
| 70 | + get("/userInfo") { |
| 71 | + call.respond(call.getUser()) |
63 | 72 | }
|
64 | 73 | }
|
65 | 74 | }
|
66 | 75 | }
|
67 | 76 |
|
68 |
| -class UserSession(accessToken: String) |
| 77 | +fun ApplicationCall.getUser(): User = |
| 78 | + getUserOrNull() ?: throw UnauthorizedException("User not authenticated") |
| 79 | + |
| 80 | +fun ApplicationCall.getUserOrNull(): User? { |
| 81 | + val principal = authentication.principal<JWTPrincipal>() ?: return null |
| 82 | + val claims = principal.payload.claims |
| 83 | + return User( |
| 84 | + id = claims["sub"]?.asString() ?: "unknown", |
| 85 | + name = claims["name"]?.asString() ?: "unknown", |
| 86 | + email = claims["email"]?.asString() ?: "unknown" |
| 87 | + ) |
| 88 | +} |
| 89 | + |
| 90 | +@Serializable |
| 91 | +data class User( |
| 92 | + val id: String, |
| 93 | + val name: String, |
| 94 | + val email: String, |
| 95 | +) |
0 commit comments