ScalaJS OAuth
Authentication Library
ScalaJS OAuth
Overview
ScalaJS OAuth is a client-side authentication library for implementing OAuth 2.0 authentication in Scala.js applications. It provides robust OAuth authentication flows through integration with modern TypeScript-based OAuth clients and utilization of JavaScript libraries.
Details
ScalaJS OAuth assists in implementing OAuth 2.0 authentication within the Scala.js ecosystem. While dedicated Scala.js OAuth libraries are limited, practical OAuth authentication systems can be built by leveraging JavaScript's rich OAuth library ecosystem and Scala.js's JavaScript interoperability. Particularly, approaches include integration with modern OAuth 2.0 libraries implemented in TypeScript like Better Auth and OIDC Client TS, utilization of existing OAuth libraries through JavaScript facade patterns, and custom implementations using Scala.js native HTTP clients. You can implement frontend-specific authentication flows while leveraging knowledge from Scala JVM libraries like sttp-oauth2.
Pros and Cons
Pros
- Scala Integration: Type-safe frontend development with Scala.js
- JavaScript Compatibility: Interoperability with rich JavaScript OAuth libraries
- Modern OAuth: Support for latest standards like OAuth 2.1, PKCE, OpenID Connect
- Facade Pattern: Creation of type-safe wrappers for existing JS libraries
- Framework Integration: Integration with React, Vue, Angular, etc.
- Type Safety: Enhanced safety through Scala's powerful type system
- Functional Programming: Utilization of Scala's functional paradigm
Cons
- Limited Dedicated Libraries: Limited choices for ScalaJS-specific OAuth libraries
- JS Dependencies: Complexity due to dependency on JavaScript libraries
- Learning Curve: Requires knowledge of both ScalaJS and OAuth
- Community Size: Smaller community compared to JavaScript
- Configuration Complexity: Complex build configuration and JS library integration
- Debugging Difficulty: Challenging debugging in cross-compilation environment
- Performance: Overhead from JavaScript library dependencies
Key Links
- Scala.js Official Site
- sttp-oauth2 (Scala JVM OAuth Reference)
- Better Auth
- OIDC Client TS
- OAuth.net - Scala Libraries
- ScalaJS JavaScript Interoperability
Usage Examples
JavaScript OAuth Library Facade
// JavaScript OAuth library facade definition
import scala.scalajs.js
import scala.scalajs.js.annotation._
@js.native
@JSImport("@auth/client", JSImport.Default)
object AuthClient extends js.Object {
def createAuthClient(config: AuthConfig): AuthClientInstance = js.native
}
@js.native
trait AuthConfig extends js.Object {
val baseURL: String = js.native
val credentials: js.Object = js.native
}
@js.native
trait AuthClientInstance extends js.Object {
def signIn(): js.Promise[AuthResponse] = js.native
def signOut(): js.Promise[Unit] = js.native
def getUser(): js.Promise[js.UndefOr[User]] = js.native
}
// Usage in Scala.js
object OAuth {
def createClient(baseURL: String): AuthClientInstance = {
val config = js.Dynamic.literal(
baseURL = baseURL,
credentials = js.Dynamic.literal(
clientId = "your-client-id",
redirectUri = "http://localhost:3000/callback"
)
).asInstanceOf[AuthConfig]
AuthClient.createAuthClient(config)
}
}
PKCE Flow Implementation
// PKCE (Proof Key for Code Exchange) implementation
import scala.scalajs.js
import scala.util.Random
import java.util.Base64
import java.security.MessageDigest
class PKCEOAuthClient(
clientId: String,
authEndpoint: String,
tokenEndpoint: String,
redirectUri: String
) {
private var codeVerifier: Option[String] = None
def generateCodeVerifier(): String = {
val bytes = new Array[Byte](32)
Random.nextBytes(bytes)
val verifier = Base64.getUrlEncoder.withoutPadding()
.encodeToString(bytes)
codeVerifier = Some(verifier)
verifier
}
def generateCodeChallenge(verifier: String): String = {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(verifier.getBytes("UTF-8"))
Base64.getUrlEncoder.withoutPadding().encodeToString(hash)
}
def getAuthorizationUrl(scope: String = "openid email profile"): String = {
val verifier = generateCodeVerifier()
val challenge = generateCodeChallenge(verifier)
val state = generateState()
val params = Map(
"response_type" -> "code",
"client_id" -> clientId,
"redirect_uri" -> redirectUri,
"scope" -> scope,
"state" -> state,
"code_challenge" -> challenge,
"code_challenge_method" -> "S256"
)
s"$authEndpoint?${params.map { case (k, v) => s"$k=${js.URIUtils.encodeURIComponent(v)}" }.mkString("&")}"
}
private def generateState(): String = {
Random.alphanumeric.take(16).mkString
}
}
OAuth Authentication Flow Management
import scala.concurrent.Future
import scala.scalajs.js
import org.scalajs.dom
import org.scalajs.dom.ext.Ajax
// OAuth authentication state management
case class AuthState(
isAuthenticated: Boolean,
user: Option[User],
accessToken: Option[String],
refreshToken: Option[String]
)
case class User(
id: String,
email: String,
name: String
)
class OAuthManager(client: PKCEOAuthClient) {
private var currentState = AuthState(false, None, None, None)
private val stateKey = "oauth-state"
def initiateLogin(): Unit = {
val authUrl = client.getAuthorizationUrl()
dom.window.location.href = authUrl
}
def handleCallback(code: String, state: String): Future[AuthState] = {
exchangeCodeForToken(code).map { tokenResponse =>
val newState = AuthState(
isAuthenticated = true,
user = Some(parseUserFromToken(tokenResponse.idToken)),
accessToken = Some(tokenResponse.accessToken),
refreshToken = tokenResponse.refreshToken
)
persistState(newState)
currentState = newState
newState
}
}
private def exchangeCodeForToken(code: String): Future[TokenResponse] = {
val data = js.Dynamic.literal(
grant_type = "authorization_code",
client_id = client.clientId,
code = code,
redirect_uri = client.redirectUri,
code_verifier = client.codeVerifier.getOrElse("")
)
Ajax.post(
url = client.tokenEndpoint,
data = js.JSON.stringify(data),
headers = Map("Content-Type" -> "application/json")
).map { xhr =>
val response = js.JSON.parse(xhr.responseText)
TokenResponse(
accessToken = response.access_token.asInstanceOf[String],
refreshToken = Option(response.refresh_token.asInstanceOf[String]),
idToken = response.id_token.asInstanceOf[String]
)
}
}
private def parseUserFromToken(idToken: String): User = {
// JWT decoding and user information extraction
val payload = decodeJWT(idToken)
User(
id = payload.sub,
email = payload.email,
name = payload.name
)
}
def logout(): Unit = {
currentState = AuthState(false, None, None, None)
clearPersistedState()
}
private def persistState(state: AuthState): Unit = {
dom.window.localStorage.setItem(stateKey, js.JSON.stringify(state))
}
private def clearPersistedState(): Unit = {
dom.window.localStorage.removeItem(stateKey)
}
}
case class TokenResponse(
accessToken: String,
refreshToken: Option[String],
idToken: String
)
React Integration Usage
// ScalaJS React OAuth integration
import slinky.core._
import slinky.core.annotations.react
import slinky.web.html._
@react class LoginComponent extends Component {
type Props = Unit
case class State(
isLoading: Boolean = false,
user: Option[User] = None,
error: Option[String] = None
)
private val oauthManager = new OAuthManager(
new PKCEOAuthClient(
clientId = "your-client-id",
authEndpoint = "https://auth.example.com/oauth/authorize",
tokenEndpoint = "https://auth.example.com/oauth/token",
redirectUri = dom.window.location.origin + "/callback"
)
)
def initialState: State = State()
def render(): ReactElement = {
div(
state.user match {
case Some(user) =>
div(
p(s"Welcome, ${user.name}!"),
p(s"Email: ${user.email}"),
button(
onClick := (_ => oauthManager.logout()),
"Logout"
)
)
case None =>
div(
h2("Please Login"),
button(
onClick := (_ => handleLogin()),
disabled := state.isLoading,
if (state.isLoading) "Logging in..." else "Login with OAuth"
),
state.error.map(err => p(style := js.Dynamic.literal(color = "red"), err))
)
}
)
}
private def handleLogin(): Unit = {
setState(_.copy(isLoading = true, error = None))
oauthManager.initiateLogin()
}
}
Secure API Calls
// Authenticated API calls
import scala.concurrent.Future
import org.scalajs.dom.ext.Ajax
class AuthenticatedApiClient(oauthManager: OAuthManager) {
def makeAuthenticatedRequest[T](
url: String,
method: String = "GET",
data: Option[String] = None
)(decoder: String => T): Future[T] = {
oauthManager.currentState.accessToken match {
case Some(token) =>
val headers = Map(
"Authorization" -> s"Bearer $token",
"Content-Type" -> "application/json"
)
val request = method.toUpperCase match {
case "GET" => Ajax.get(url, headers = headers)
case "POST" => Ajax.post(url, data.getOrElse(""), headers = headers)
case "PUT" => Ajax.put(url, data.getOrElse(""), headers = headers)
case "DELETE" => Ajax.delete(url, headers = headers)
case _ => throw new IllegalArgumentException(s"Unsupported method: $method")
}
request.map(xhr => decoder(xhr.responseText))
.recover {
case _: Exception if xhr.status == 401 =>
// Handle invalid token
oauthManager.logout()
throw new Exception("Authentication required")
}
case None =>
Future.failed(new Exception("Not authenticated"))
}
}
// Usage example
def getUserProfile(): Future[UserProfile] = {
makeAuthenticatedRequest("/api/user/profile") { response =>
val json = js.JSON.parse(response)
UserProfile(
id = json.id.asInstanceOf[String],
name = json.name.asInstanceOf[String],
email = json.email.asInstanceOf[String],
avatar = Option(json.avatar.asInstanceOf[String])
)
}
}
}
case class UserProfile(
id: String,
name: String,
email: String,
avatar: Option[String]
)
Token Refresh Functionality
// Automatic token refresh
class TokenManager(
refreshEndpoint: String,
clientId: String
) {
def refreshAccessToken(refreshToken: String): Future[TokenResponse] = {
val data = js.Dynamic.literal(
grant_type = "refresh_token",
client_id = clientId,
refresh_token = refreshToken
)
Ajax.post(
url = refreshEndpoint,
data = js.JSON.stringify(data),
headers = Map("Content-Type" -> "application/json")
).map { xhr =>
val response = js.JSON.parse(xhr.responseText)
TokenResponse(
accessToken = response.access_token.asInstanceOf[String],
refreshToken = Option(response.refresh_token.asInstanceOf[String]),
idToken = response.id_token.asInstanceOf[String]
)
}
}
// API client with auto-refresh functionality
def withAutoRefresh[T](
apiCall: String => Future[T]
)(accessToken: String, refreshToken: Option[String]): Future[T] = {
apiCall(accessToken).recoverWith {
case _: Exception => // For 401 Unauthorized
refreshToken match {
case Some(rt) =>
refreshAccessToken(rt).flatMap { newTokens =>
apiCall(newTokens.accessToken)
}
case None =>
Future.failed(new Exception("No refresh token available"))
}
}
}
}