В это пример я буду рассматривать только парсинг и валидацию токенов что уже пришли в мое API в Authorization хедере. Для генерации токенов, регистрации пользователей и прочего SSO есть много готовых решений которые легко установить или даже устанавливать не надо. Например, Auth0, Keyckloak, IdentityServer4. В пример е буду работать с Tapir который может использовать в качестве бекенда http4s, Akk HTTP, Netty, Finatra, Play, ZIO Http, Armeria. Я буду использовать Tapir + Http4s.

Полистав интернет я выяснил что самой полулярной библиотекой для этого является PAC4J который внутри себя использует oauth2-oidc-sdk.

Исходный код лежит тут - https://gitlab.com/VictorWinbringer/scalaauth

Сначала нужно установить sbt. Проще всего по этой инструкции https://www.scala-lang.org/download/

Создаем проект командой (работает и на Windows).

sbt new https://codeberg.org/wegtam/http4s-tapir.g8.git

Проект создаться сразу с поддержкой БД Постгрес. Сейчас нам БД не нужна поэтому закомментирую строку, запускающую миграции БД в файле Server.scala

 //_ <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass) 

Добавим в файл build.sbt oauth2-oidc-sdk

"com.nimbusds" % "oauth2-oidc-sdk" % "9.27"

Создаем тип для нашего токена. Тут используем GitHub - fthomas/refined: Refinement types for Scala для описания ValueObject нашего который может быть только не пустой строкой.

type AuthToken = String Refined NonEmpty

object AuthToken extends RefinedTypeOps[AuthToken, String] with CatsRefinedTypeOpsSyntax

создаем тип для ошибки

final case class Error(msg: String, statusCode: StatusCode) extends Exception

Создаем базовый эндпойнт для эндпойнтов требующих авторизацию

val baseEndpoint = endpoint
	.in("api")
	.in("v1")
	.errorOut(stringBody.and(statusCode).mapTo[Error]) 

val baseEndpointWithAuth = baseEndpoint
	.in(auth.bearer[AuthToken]())

Сама логика валидации. Функция parse. Если токен валидный, то возвращается идентификатор пользователя. Поле “sub” токена. Иначе будет возвращена ошибка

 def authWithToken(token: AuthToken) = Try[String]({
      //Адрес SSO сервера
      val iss = new Issuer("https://dev-t-ca3k92.us.auth0.com/")
      //Идентификатор нашего клиента он же Audience
      val clientID = new ClientID("http://127.0.0.1:8888")
      val jwsAlg = JWSAlgorithm.RS256
      //Адрес по которому загружать данные для JWK
      val jwkSetURL = new URL("https://dev-t-ca3k92.us.auth0.com/.well-known/jwks.json")
      val validator = new IDTokenValidator(iss, clientID, jwsAlg, jwkSetURL, new DefaultResourceRetriever())
      val idToken = JWTParser.parse(token.value)
      //Валидируем токен и получаем сохраненные в нем данные.
      //Идет проверка времени жизни и других параметров 
      val claims = validator.validate(idToken, null)
      claims.getSubject().getValue
    }).toEither
      .swap
      .map(x => x.getMessage)
      .swap
      .flatMap(x => UserId.from(x))

Исползуем его в эндпойне что будет возвращать этот самый идентификатор пользователя

private val getMySub = BaseController.baseEndpointWithAuth   
	.in("hello")   
	.tag("Hello")   
	.in("sub")   
	.serverLogic(x=>IO(
		authWithToken(x)
		.swap
		.map(x=> Error(x.getMessage, StatusCode.Unauthorized))
		.swap
		))

Так же Tapir предоставляет возможность делать цепочку вычислений через метод serverLogicForCurrent

Например, в первом методе можно распарсить токен а во втором уже проверить роли пользователя и уже работать с нужными данными. Например, вот так.

//Возвращает 401 если не удалось провалидировать токен пользователя 
def authenticate(token: AuthToken): IO[Either[Error, User]] = ???  
//Возвращает 403 если у пользователя нет ни одной роли из писка 
def authorize(user: User, roles: Seq[String]): IO[Either[Error, User]] = ???  
private val getUserProfile = BaseController.baseEndpointWithAuth
   .get
   .in("hello")
   .tag("Hello")
   .in("profile")
   .serverLogicForCurrent(authenticate)
   .out(jsonBody[User])
   .serverLogic({ case (user, _) => authorize(user, Seq("admin", "root")) })

Чтобы сгенерировать сам токен доступа заходим в наш дашбоард Auth0 и добавляем приложение.

https://manage.auth0.com/dashboard/

 Потом добавляем API

Переходим в раздел для тестирования и копируем токен

Дальше уже можем его использовать в заголовке AccesToken: Bearer {наш токен}

Исходники

Комментарии (0)