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