Authorization. API protection with Bearer token

In this example, I will only consider parsing and validating tokens that have already come to my API in the Authorization header. For generating tokens, registering users and other SSO, there are many ready-made solutions that are easy to install or even do not need to be installed. For example, Auth0, Keyckloak, IdentityServer4. For example, I will work with Tapir which can use as a backend http4s, Akk HTTP, Netty, Finatra, Play, ZIO Http, Armeria. I will be using Tapir + Http4s.

Having looked through the Internet, I found out that the most popular library for this is PAC4J which uses internally oauth2-oidc-sdk.

The source code is here – https://gitlab.com/VictorWinbringer/scalaauth

First you need to install sbt. The easiest way is to follow this instruction https://www.scala-lang.org/download/

Create a project with a team (works on Windows too).

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

The project will be created immediately with Postgres database support. Now we don’t need a database, so I’ll comment out the line that starts database migrations in the Server.scala file

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

Add oauth2-oidc-sdk to build.sbt file

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

We create a type for our token. Here we use GitHub – fthomas/refined: Refinement types for Scala to describe our ValueObject which can only be a non-empty string.

type AuthToken = String Refined NonEmpty

object AuthToken extends RefinedTypeOps[AuthToken, String] with CatsRefinedTypeOpsSyntax

create type for error

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

Create a base endpoint for endpoints requiring authorization

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

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

The validation logic itself. parse function. If the token is valid, then the user ID is returned. The “sub” field of the token. Otherwise, an error will be returned.

 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))

We use it in the endpoint that will return this same user ID

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 also provides the ability to do a chain of calculations through the method serverLogicForCurrent

For example, in the first method, you can parse the token, and in the second, you can already check the user’s roles and already work with the necessary data. For example, like this.

//Возвращает 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")) })

To generate the access token itself, go to our Auth0 dashboard and add the application.

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

Then add the API

Go to the testing section and copy the token

Then we can use it in the header AccesToken: Bearer {our token}

Sources

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *