Scala: Авторизация. Защита API с помошью Bearer токена

от автора

В это пример я буду рассматривать только парсинг и валидацию токенов что уже пришли в мое 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 {наш токен}

Исходники


ссылка на оригинал статьи https://habr.com/ru/post/655081/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *