feat: auth and redirect landing create user

This commit is contained in:
efim 2023-07-23 18:27:49 +00:00
parent f742acd6f9
commit ef6646e004
4 changed files with 109 additions and 16 deletions

View File

@ -623,3 +623,7 @@ video {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity)); color: rgb(37 99 235 / var(--tw-text-opacity));
} }
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}

View File

@ -2,6 +2,8 @@ package example
import AuthService._ import AuthService._
import example.pocketbase.Api import example.pocketbase.Api
import upickle.default._
import example.pocketbase.Models._
case class AuthService()(implicit cc: castor.Context, log: cask.Logger) case class AuthService()(implicit cc: castor.Context, log: cask.Logger)
extends cask.Routes { extends cask.Routes {
@ -23,14 +25,18 @@ case class AuthService()(implicit cc: castor.Context, log: cask.Logger)
@cask.get("/login") @cask.get("/login")
def getLoginPage() = { def getLoginPage() = {
// render auth page with the available oauth providers // render auth page with the available oauth providers
val authOptions = pocketbaseApi.listAuthMethods().authProviders val authOptions = pocketbaseApi.listAuthMethods()
val options = s"got following auth opitons: $authOptions" val options = s"got following auth opitons: $authOptions"
// save states and verifiers into cookie // save states and verifiers into cookie
val githubOption = authOptions.find(_.name == "github") val oauthVerificationCookie = authOptions.toOauthCookieInfo()
val yoyo = write(oauthVerificationCookie)
val githubRedirect = githubOption.map(_.authUrl).getOrElse("") ++ getRedirectUrl("github") val githubOption = authOptions.authProviders.find(_.name == "github")
val githubRedirect =
githubOption.map(_.authUrl).getOrElse("") ++ getRedirectUrl("github")
val html = s""" val html = s"""
<h1>good enough, right</h1> <h1>good enough, right</h1>
@ -41,12 +47,59 @@ case class AuthService()(implicit cc: castor.Context, log: cask.Logger)
cask.Response( cask.Response(
html, html,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8"), headers = Seq("Content-Type" -> "text/html;charset=UTF-8"),
cookies = Seq(cask.Cookie(name = oauthVerifiersCookieName, value = yoyo))
) )
} }
/** @oauthVerifiers
* \- required cookie from start of oauth initialization cask will fail
* request if this is not present
*/
@cask.get(s"${baseRedirectUrl}/:provider") @cask.get(s"${baseRedirectUrl}/:provider")
def receiveOauthRedirect(provider: String, state: String, code: String) = { def receiveOauthRedirect(
println(s"received redirect for $provider with state: $state and code: $code") provider: String,
state: String,
code: String,
request: cask.Request
) = {
println(
s"received redirect for $provider with state: $state and code: $code"
)
val authVerifierOpt = request.cookies
.get(oauthVerifiersCookieName)
.map(cookie => read[OauthInfoCookie](cookie.value))
.flatMap(_.providersVerification.find(_.name == provider))
val resultOpt = for {
authVerifier <- authVerifierOpt
_ <- Some(()).filter(_ => state == authVerifier.state)
pocketbaseAuthResult <- pocketbaseApi
.authWithOauth(
provider = provider,
code = code,
verifier = authVerifier.codeVerifier,
redirectUrl = getRedirectUrl(provider)
)
.toOption
} yield pocketbaseAuthResult
val okMessageFirst = resultOpt match {
case None =>
// i guess with the SSR i'll need to return message about unsuccessful auth?
s"""
<h1>Auth unsuccessful</h1>
"""
case Some(result) =>
// this is already fully successful auth
s"""
<h1>Ok, good</h1>
<p>user should be already created, current jwt : ${result.token}</p>
<p>the account is on ${result.record.email} and ${result.record.username}</p>
"""
}
/* /*
* *
get provider from path param, get verifiers and state from cookie, get provider from path param, get verifiers and state from cookie,
@ -59,7 +112,18 @@ and delete the state\verifiers cookie
but then what? i guess call for redirect to root page again? but then what? i guess call for redirect to root page again?
which should trigger auth check and main page render? which should trigger auth check and main page render?
*/ */
s"received redirect for $provider with state: $state and code: $code" import java.time.Instant
cask.Response(
okMessageFirst,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8"),
cookies = Seq(
cask.Cookie(
name = oauthVerifiersCookieName,
value = "",
expires = Instant.now()
)
)
)
} }
initialize() initialize()
@ -67,6 +131,12 @@ which should trigger auth check and main page render?
object AuthService { object AuthService {
val authCookieName = "auth" val authCookieName = "auth"
// this is to share info between /auth page and /redirect-landing
// can be improved by generating same state and codeVerifier for all oauth links
// and can also be stored purely on the backend - a tad more secure, i guess
// but if cookie is under https, should be ok
val oauthVerifiersCookieName = "oauthVerifiers"
val pocketbaseApi = Api("http://127.0.0.1:8090") val pocketbaseApi = Api("http://127.0.0.1:8090")
val selfUri = "http://127.0.0.1:8080" val selfUri = "http://127.0.0.1:8080"

View File

@ -47,7 +47,7 @@ final case class Api(pocketbaseUrl: String, usersCollection: String = "users") {
"provider" -> provider, "provider" -> provider,
"code" -> code, "code" -> code,
"codeVerifier" -> verifier, "codeVerifier" -> verifier,
"redirectUri" -> redirectUrl "redirectUrl" -> redirectUrl
) )
) )

View File

@ -4,6 +4,22 @@ import upickle.default._
object Models { object Models {
final case class AvailableAuthMethods(
usernamePassword: Boolean,
emailPassword: Boolean,
authProviders: List[AuthProviderInfo]
) derives ReadWriter {
def toOauthCookieInfo() = OauthInfoCookie(
providersVerification = authProviders.map(provider =>
AuthProviderVerification(
name = provider.name,
state = provider.state,
codeVerifier = provider.codeVerifier
)
)
)
}
final case class AuthProviderInfo( final case class AuthProviderInfo(
name: String, name: String,
state: String, state: String,
@ -13,12 +29,15 @@ object Models {
authUrl: String authUrl: String
) derives ReadWriter ) derives ReadWriter
final case class AvailableAuthMethods( final case class OauthInfoCookie(
usernamePassword: Boolean, providersVerification: List[AuthProviderVerification]
emailPassword: Boolean,
authProviders: List[AuthProviderInfo]
) derives ReadWriter ) derives ReadWriter
final case class AuthProviderVerification(
name: String,
state: String,
codeVerifier: String,
) derives ReadWriter
// auth methods // auth methods
/* /*
@ -39,6 +58,11 @@ object Models {
* *
*/ */
final case class AuthReply(
token: String,
record: BaseAccountData
) derives ReadWriter
final case class BaseAccountData( final case class BaseAccountData(
id: String, id: String,
collectionId: String, collectionId: String,
@ -51,11 +75,6 @@ object Models {
emailVisibility: Boolean emailVisibility: Boolean
) derives ReadWriter ) derives ReadWriter
final case class AuthReply(
token: String,
record: BaseAccountData
) derives ReadWriter
// auth reply // auth reply
/* /*
* *