diff --git a/src/main/resources/out.css b/src/main/resources/out.css index ed795c8..65a17c3 100644 --- a/src/main/resources/out.css +++ b/src/main/resources/out.css @@ -623,3 +623,7 @@ video { --tw-text-opacity: 1; 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); +} diff --git a/src/main/scala/example/AuthService.scala b/src/main/scala/example/AuthService.scala index 38c2428..d5762ab 100644 --- a/src/main/scala/example/AuthService.scala +++ b/src/main/scala/example/AuthService.scala @@ -2,6 +2,8 @@ package example import AuthService._ import example.pocketbase.Api +import upickle.default._ +import example.pocketbase.Models._ case class AuthService()(implicit cc: castor.Context, log: cask.Logger) extends cask.Routes { @@ -23,14 +25,18 @@ case class AuthService()(implicit cc: castor.Context, log: cask.Logger) @cask.get("/login") def getLoginPage() = { // 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" // 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"""

good enough, right

@@ -41,12 +47,59 @@ case class AuthService()(implicit cc: castor.Context, log: cask.Logger) cask.Response( html, 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") - def receiveOauthRedirect(provider: String, state: String, code: String) = { - println(s"received redirect for $provider with state: $state and code: $code") + def receiveOauthRedirect( + 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""" +

Auth unsuccessful

+""" + case Some(result) => + // this is already fully successful auth + s""" +

Ok, good

+

user should be already created, current jwt : ${result.token}

+

the account is on ${result.record.email} and ${result.record.username}

+""" + } + /* * 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? 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() @@ -67,6 +131,12 @@ which should trigger auth check and main page render? object AuthService { 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 selfUri = "http://127.0.0.1:8080" diff --git a/src/main/scala/example/pocketbase/Api.scala b/src/main/scala/example/pocketbase/Api.scala index 9801cd4..76ea9e2 100644 --- a/src/main/scala/example/pocketbase/Api.scala +++ b/src/main/scala/example/pocketbase/Api.scala @@ -47,7 +47,7 @@ final case class Api(pocketbaseUrl: String, usersCollection: String = "users") { "provider" -> provider, "code" -> code, "codeVerifier" -> verifier, - "redirectUri" -> redirectUrl + "redirectUrl" -> redirectUrl ) ) diff --git a/src/main/scala/example/pocketbase/Models.scala b/src/main/scala/example/pocketbase/Models.scala index cd5fda0..6d20f80 100644 --- a/src/main/scala/example/pocketbase/Models.scala +++ b/src/main/scala/example/pocketbase/Models.scala @@ -4,6 +4,22 @@ import upickle.default._ 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( name: String, state: String, @@ -13,12 +29,15 @@ object Models { authUrl: String ) derives ReadWriter - final case class AvailableAuthMethods( - usernamePassword: Boolean, - emailPassword: Boolean, - authProviders: List[AuthProviderInfo] + final case class OauthInfoCookie( + providersVerification: List[AuthProviderVerification] ) derives ReadWriter + final case class AuthProviderVerification( + name: String, + state: String, + codeVerifier: String, + ) derives ReadWriter // auth methods /* @@ -39,6 +58,11 @@ object Models { * */ + final case class AuthReply( + token: String, + record: BaseAccountData + ) derives ReadWriter + final case class BaseAccountData( id: String, collectionId: String, @@ -51,11 +75,6 @@ object Models { emailVisibility: Boolean ) derives ReadWriter - final case class AuthReply( - token: String, - record: BaseAccountData - ) derives ReadWriter - // auth reply /* *