All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.malliina.web.flows.scala Maven / Gradle / Ivy

package com.malliina.web

import cats.effect.{IO, Sync}
import com.malliina.http.FullUrl
import com.malliina.util.AppLogger
import com.malliina.values.{Email, ErrorMessage}
import com.malliina.web.OAuthKeys.*

import scala.concurrent.Future

class EmailAuthFlow[F[_]: Sync](conf: AuthCodeConf[F]) extends StandardAuthFlow[F, Email](conf):
  override def parse(v: Verified): Either[JWTError, Email] =
    v.readString(EmailKey).map(Email.apply)

trait AuthFlow[F[_], U] extends FlowStart[F] with CallbackValidator[F, U]

trait FlowStart[F[_]]:
  def start(redirectUrl: FullUrl, extraParams: Map[String, String]): F[Start]

  def extraRedirParams(redirectUrl: FullUrl): Map[String, String] = Map.empty

  protected def commonAuthParams(
    authScope: String,
    redirectUrl: FullUrl,
    clientId: ClientId
  ): Map[String, String] =
    Map(
      RedirectUri -> redirectUrl.url,
      ClientIdKey -> clientId.value,
      Scope -> authScope
    )

trait StaticFlowStart[F[_]: Sync] extends FlowStart[F]:
  def conf: StaticConf

  override def start(redirectUrl: FullUrl, extraParams: Map[String, String]): F[Start] =
    val params =
      commonAuthParams(conf.scope, redirectUrl, conf.authConf.clientId) ++ extraRedirParams(
        redirectUrl
      ) ++ extraParams
    Sync[F].pure(Start(conf.authorizationEndpoint, params, None))

trait LoginHint[F[_]]:
  self: FlowStart[F] =>
  def startHinted(
    redirectUrl: FullUrl,
    loginHint: Option[String],
    extraParams: Map[String, String]
  ): F[Start] =
    self.start(
      redirectUrl,
      extraParams ++ loginHint.map(lh => Map(LoginHint -> lh)).getOrElse(Map.empty)
    )

abstract class StandardAuthFlow[F[_]: Sync, V](conf: AuthCodeConf[F])
  extends DiscoveringAuthFlow[F, V](conf)
  with LoginHint[F]

object CallbackValidator:
  private val log = AppLogger(getClass)

trait CallbackValidator[F[_]: Sync, U]:
  import CallbackValidator.log

  /** Returns either a successfully validated user object or an AuthError if validation fails.
    *
    * The returned Future fails with a ResponseException if any network request fails.
    *
    * @param code
    *   auth code
    * @param redirectUrl
    *   redir url
    * @return
    *   a user object or a failure
    */
  def validate(
    code: Code,
    redirectUrl: FullUrl,
    requestNonce: Option[String]
  ): F[Either[AuthError, U]]

  def validateCallback(cb: Callback): F[Either[AuthError, U]] =
    val isStateOk = cb.requestState.exists(rs => cb.sessionState.contains(rs))
    if isStateOk then
      cb.codeQuery.map { code => validate(Code(code), cb.redirectUrl, cb.requestNonce) }.getOrElse {
        log.error(s"Authentication failed, code missing.")
        Sync[F].pure(Left(OAuthError(ErrorMessage("Code missing."))))
      }
    else
      val detailed = (cb.requestState, cb.sessionState) match
        case (Some(rs), Some(ss)) => s"Got '$rs', expected '$ss'."
        case (Some(rs), None)     => s"Got '$rs', but found nothing to compare to."
        case (None, Some(ss))     => s"No state in request, expected '$ss'."
        case _                    => "No state in request and nothing to compare to either."
      log.error(s"Authentication failed, state mismatch. $detailed")
      Sync[F].pure(Left(OAuthError(ErrorMessage("State mismatch."))))

  /** Not encoded.
    */
  protected def validationParams(
    code: Code,
    redirectUrl: FullUrl,
    conf: AuthConf
  ): Map[String, String] = Map(
    ClientIdKey -> conf.clientId.value,
    ClientSecretKey -> conf.clientSecret.value,
    RedirectUri -> redirectUrl.url,
    CodeKey -> code.code
  )




© 2015 - 2025 Weber Informatics LLC | Privacy Policy