# Kotlin and JVM SDK

> The Kotlin/JVM client and verifier, a Ktor authentication provider and a Spring Boot starter.

Three modules (JVM 17+). Not on Maven Central yet (they will be at the
public release): install them into your local Maven repository and
depend on them from your services.

```sh
cd sdk/kotlin && ./gradlew publishToMavenLocal
```

```kotlin
// your service's build.gradle.kts
repositories { mavenLocal(); mavenCentral() }
dependencies {
    implementation("io.github.burakmetehan.authbase:authbase-ktor:0.1.0")                 // Ktor
    implementation("io.github.burakmetehan.authbase:authbase-spring-boot-starter:0.1.0")  // or Spring Boot
}
```

| Module | What |
|---|---|
| `authbase-core` | `Authbase` (suspend client for the end-user API) and `AccessTokenVerifier` |
| `authbase-ktor` | a Ktor `Authentication` provider and `requireRoles` |
| `authbase-spring-boot-starter` | auto-configured client, Spring Security `JwtDecoder`, `roles` → `ROLE_…` |

## The client

One per app, with its API key from the environment (never in code):

```kotlin
val authbase = Authbase(url = System.getenv("AUTHBASE_URL"), apiKey = System.getenv("AUTHBASE_API_KEY"))

authbase.users.create(CreateUserRequest(email = email, password = password))
val tokens = authbase.auth.login(LoginRequest(identifier = email, password = password, clientIp = ip))
val next = authbase.auth.refresh(RefreshRequest(refreshToken = tokens.refreshToken))
authbase.users.update(userId) { roles = listOf("admin"); username = null } // sends only these; null clears
```

Calls are `suspend` and throw `AuthbaseException` on a non-2xx answer:
`status`, `code` (branch on this), `detail`, `requestId`, `errors` (per
field, for 422), `retryAfter` (429/503). An unreachable server is
`code = "network_error"`.

## Verifying access tokens

Offline, against the app's JWKS: fetched once, cached for 5 minutes,
refetched (at most once a second) for an unknown `kid`, so a signing-key
rotation needs no restart. Only `EdDSA`, `typ: at+jwt`, the app's issuer
and the app id as `aud` are accepted. Ed25519 is verified with the JDK's
own implementation (Java 15+), so no Tink or BouncyCastle is pulled in.

```kotlin
val claims = authbase.verifyAccessToken(token)           // learns issuer and app id from the API key, once
val verifier = AccessTokenVerifier(VerifierOptions(issuer = "https://auth.example.com/apps/cashmate", appId = "01a0…"))
```

A bad token throws `AuthbaseTokenException` (`reason`: `MISSING`,
`EXPIRED` — worth a refresh — or `INVALID`); an unreachable JWKS throws
`AuthbaseUnavailableException`, an outage rather than the caller's fault.

## Ktor

```kotlin
install(Authentication) { authbase { client(authbase) } }   // or verifier(VerifierOptions(issuer, appId))
routing {
    authenticate("authbase") {
        get("/me") { call.respond(call.authbase!!.subject) }
        requireRoles("admin") { delete("/posts/{id}") { /* … */ } }
    }
}
```

401 (`WWW-Authenticate: Bearer`, RFC 6750) for a missing or bad token, 403
for missing roles, 503 when the JWKS cannot be fetched; bodies are
problem+json.

## Spring Boot

Add `spring-boot-starter-security-oauth2-resource-server` and set either
the client's properties or the verifier's:

```properties
authbase.url=https://auth.example.com
authbase.api-key=${AUTHBASE_API_KEY}
# or, to verify without an API key:
# authbase.issuer=https://auth.example.com/apps/cashmate
# authbase.app-id=01a0…
```

```kotlin
@Bean
fun security(http: HttpSecurity): SecurityFilterChain = http
    .authorizeHttpRequests { it.requestMatchers("/admin/**").hasRole("admin"); it.anyRequest().authenticated() }
    .oauth2ResourceServer { it.jwt { } }
    .build()
```

The starter supplies the `JwtDecoder` (authbase's verifier) and maps the
`roles` claim to `ROLE_…` authorities; `@AuthenticationPrincipal jwt: Jwt`
gives the claims (`jwt.subject` is the user id). An `Authbase` bean is
there for calls; its methods are `suspend`, so Spring MVC controllers that
call them need `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` (Spring
runs suspend handlers through Reactor). Define your own bean of any of
these types to replace it.

## Development

`./gradlew build` compiles (warnings are errors), tests and packages every
module. `ModelsMatchSpecTest` fails when a model's fields drift from
`api/openapi/openapi.yaml`. `AUTHBASE_TEST_URL=http://localhost:8080
./gradlew test` also runs the integration test against a real instance.
