komposeauth

Full-stack authentication stack: ready-to-run server plus shared SDK and client SDK with federated Google, passkey, OTP, email verification, KYC, reusable UI and credential manager.

Android
JVM
iOS
Linux
Wasm
JS
GitHub stars21
Dependents0
LicenseApache License 2.0
Creation dateabout 1 year ago

Last activity6 days ago
Latest release0.2.6 (about 2 months ago)

komposeauth

Full-stack auth for Kotlin Multiplatform: Spring Auth Server + KMP SDK + Client SDK

Maven Central (shared) Maven Central (client) Docker Compose Multiplatform License: Apache-2.0


Overview

  • Server: Spring Boot Authorization Application
  • Shared KMP SDK: Shared DTOs and utilities to be used by client and server
  • Client CMP SDK: Ktor, ViewModels, platform utilities, and reusable UI components

Features

  • Federated authorization with Google
  • username/password
  • passkey
  • email verification
  • Phone OTP
  • KYC
  • Sentry
  • Swagger/OpenAPI

Quickstart

1) Run the Server (Docker)

// BASE64_ENCRYPTION_KEY generator
openssl rand -base64 32
docker pull pitampoudel/komposeauth:latest
# Quick start
docker run -p 80:8080 \
  -e MONGODB_URI="mongodb://your-mongo-host:27017" \
  -e BASE64_ENCRYPTION_KEY="<paste-your-base64-key>" \
  pitampoudel/komposeauth:latest
  • The data lives in a database named komposeauth, fixed in application.yml, so MONGODB_URI does not name one.

  • After the container is running, open the configuration page to set up everything else:

    The key is the same BASE64_ENCRYPTION_KEY you started the container with. It is needed because no account exists yet and this page reads and writes every secret the server holds — SMTP password, SMS provider token, OAuth client secrets — so it is never open to an unauthenticated visitor, not even on a fresh install. Once you have created an account and given it the ADMIN role, signing in is enough and the key is no longer required.

    To keep the key out of your browser history and any proxy logs, you can send it as a header instead:

    curl -H "X-Master-Key: <paste-your-base64-key>" http://localhost/admin/config

Tell the server how it is reached

The abuse limits count per client address, and the server can only work out which address that is if it knows what stands between it and the internet. X-Forwarded-For is written by the caller as much as by any proxy, so entries are trustworthy only from the right-hand end inwards — and only as far in as the proxies you actually run. TRUSTED_PROXY_COUNT is how many that is.

Hosting platforms do this in one of two ways, and they need opposite settings.

Some edges publish the client address under a header of their own. Name it and it's used as-is — no counting, nothing of the caller's mixed in. Prefer this wherever it's offered:

Platform Setting
Railway CLIENT_IP_HEADER=X-Real-IP
Fly.io CLIENT_IP_HEADER=Fly-Client-IP
Behind Cloudflare CLIENT_IP_HEADER=CF-Connecting-IP

Other edges append to X-Forwarded-For, leaving whatever the caller sent to the left of their own entries. There, count hops in from the right:

Deployment Setting
Google Cloud Run, at its own run.app URL TRUSTED_PROXY_COUNT=1
Behind a GCP external Application Load Balancer TRUSTED_PROXY_COUNT=2
Your own nginx / Caddy in front TRUSTED_PROXY_COUNT=1, plus one per extra hop
Exposed directly, as in the quickstart above leave both unset, and set FORWARD_HEADERS_STRATEGY=none

Count only proxies you control. Guessing too high is the safe direction — the server falls back to the connection's own peer address. Guessing too low attributes every request to your proxy, so one shared budget covers all your users and the limits refuse them together.

Getting this wrong is not cosmetic: trust a header the edge does not overwrite and callers simply nominate who gets counted, so the limits stop working while still appearing to be on. The server logs a warning naming the relevant setting when it can tell something is off, but it cannot detect every case.

Checking it, rather than trusting the table

Providers change, they disagree with their own documentation, and putting a CDN in front changes the answer again. Once deployed, sign in as an admin and call:

curl https://your-auth-server/admin/client-ip -H "Cookie: <your session>"

It reports the address the limits are currently counting you as, how that was decided, and every client-address header the request actually carried. Call it from a phone on mobile data — somewhere the public address is unmistakably yours — and set CLIENT_IP_HEADER to whichever header came back holding it. If instead X-Forwarded-For ends with your address, count its position from the right and use TRUSTED_PROXY_COUNT.

The last row is the only one that should turn off FORWARD_HEADERS_STRATEGY. Everywhere else it must stay at its default of framework, because that is what tells the server it was reached over HTTPS — without it, session cookies lose Secure, cross-site sign-in stops working, and verification emails carry http:// links.

Cloud Run
gcloud run deploy komposeauth \
  --image pitampoudel/komposeauth:latest \
  --set-env-vars MONGODB_URI="mongodb+srv://...",BASE64_ENCRYPTION_KEY="<your-base64-key>",TRUSTED_PROXY_COUNT=1 \
  --min-instances 0 \
  --concurrency 40 \
  --cpu 1 --memory 1Gi \
  --cpu-boost \
  --startup-probe httpGet.path=/actuator/health/readiness,httpGet.port=8080,initialDelaySeconds=4,periodSeconds=2,timeoutSeconds=2,failureThreshold=45

scripts/deploy-cloud-run.sh does all of this from deploy-targets.json, which is worth using once you have more than one target.

TRUSTED_PROXY_COUNT=1 is what Cloud Run needs: its front end appends the caller's address as the last X-Forwarded-For entry, which is the one this server reads, and sets X-Forwarded-Proto: https for the default framework strategy to pick up. Use 2 instead if you front the service with an external Application Load Balancer, which appends both the client address and its own forwarding rule.

Scaling to several instances is already accounted for — sessions, OAuth2 authorizations and the abuse counters all live in MongoDB rather than in one container's memory, so limits hold across instances and survive cold starts.

2) Add the SDK to your KMP project

Shared module (optional and also included already on client module)

// Check the badge above for the latest version
implementation("io.github.pitampoudel:komposeauth-shared:x.x.x")

Client module

// Check the badge above for the latest version
implementation("io.github.pitampoudel:komposeauth-client:x.x.x")

HttpClient example (at each platform)

val httpClient = HttpClient {
    installKomposeAuth(
        authServerUrl = "https://your-auth-server",
        resourceServerUrls = listOf(
            "https://your-resource-server"
        )
    )
}

Initialize SDK

initializeKomposeAuth(
    httpClient = httpClient
)

Integrating with your apps (guide for LLMs and developers)

A complete integration guide, written so an LLM coding assistant can follow it, covers frontend sign-in (OAuth 2.1 authorization code + PKCE for SPAs, server-rendered and mobile apps, or the first-party /login API), the token claims, and resource-server token validation for Spring Boot, Node.js, Python, Go and Ktor.

Every running server also publishes both, without authentication, at https://your-auth-server/llms.txt and https://your-auth-server/llms-full.txt. Point your assistant at one of those URLs.

Usage snippets (Client)

Utilities

  • ScreenStateWrapper(...) with InfoDialog and Progress dialog
  • CountryPicker(...), DateTimeField(...), OTPTextField(...)
  • rememberFilePicker(input, selectionMode, onPicked)
  • rememberKmpCredentialManager()
  • registerSmsOtpRetriever(onRetrieved)
  • (ENUM, GeneralValidationError).toStringRes()

Current user

val userState = rememberCurrentUser()

Login with Credential Manager

val vm = koinViewModel<LoginViewModel>()
val state = vm.state.collectAsStateWithLifecycle().value
val credentialManager = rememberKmpCredentialManager()
LaunchedEffect(state.loginConfig) {
    state.loginConfig?.let {
        when (val result = credentialManager.getCredential(it)) {
            is Result.Error -> vm.onEvent(LoginEvent.ShowInfoMsg(result.message))
            is Result.Success<Credential> -> vm.onEvent(LoginEvent.Login(result.data))
        }
    }
}

OTP

val vm = koinViewModel<OtpViewModel>()
registerSmsOtpRetriever { code ->
    // vm.onEvent(OtpEvent.CodeChanged(code))
}

Profiles and KYC

val profileVm = koinViewModel<ProfileViewModel>()
val kycVm = koinViewModel<KycViewModel>()

Contributing

  • Issues and PRs are welcome
  • Please run ./gradlew build before submitting a PR
  • For larger changes, consider opening an issue first to discuss direction

Security

Cross-site requests

There are no CSRF tokens. Instead, the browser's Sec-Fetch-Site header decides:

  • Same-origin requests (this server's own pages, including the admin console) are always allowed, whatever proxy sits in front of the server.
  • Cross-site requests (anything a browser sends from another site, including a plain form post) are allowed only from origins listed under CORS allowed origins on the configuration page. With the list empty, no other site can send a cookie-authenticated write to this server.
  • Requests from outside a browser (the KMP SDK, mobile apps, backends) do not carry the header and are unaffected. Anything sending Authorization: Bearer is not forgeable anyway.

So a browser app on another origin that calls this server with credentials: "include" only needs its origin in that list.

Reporting

If you discover a security vulnerability, please email the maintainers or open a private security advisory. Avoid filing public issues with sensitive details.

License

Apache License 2.0. See LICENSE for details.

Android
JVM
iOS
Linux
Wasm
JS
GitHub stars21
Dependents0
LicenseApache License 2.0
Creation dateabout 1 year ago

Last activity6 days ago
Latest release0.2.6 (about 2 months ago)

komposeauth

Full-stack auth for Kotlin Multiplatform: Spring Auth Server + KMP SDK + Client SDK

Maven Central (shared) Maven Central (client) Docker Compose Multiplatform License: Apache-2.0


Overview

  • Server: Spring Boot Authorization Application
  • Shared KMP SDK: Shared DTOs and utilities to be used by client and server
  • Client CMP SDK: Ktor, ViewModels, platform utilities, and reusable UI components

Features

  • Federated authorization with Google
  • username/password
  • passkey
  • email verification
  • Phone OTP
  • KYC
  • Sentry
  • Swagger/OpenAPI

Quickstart

1) Run the Server (Docker)

// BASE64_ENCRYPTION_KEY generator
openssl rand -base64 32
docker pull pitampoudel/komposeauth:latest
# Quick start
docker run -p 80:8080 \
  -e MONGODB_URI="mongodb://your-mongo-host:27017" \
  -e BASE64_ENCRYPTION_KEY="<paste-your-base64-key>" \
  pitampoudel/komposeauth:latest
  • The data lives in a database named komposeauth, fixed in application.yml, so MONGODB_URI does not name one.

  • After the container is running, open the configuration page to set up everything else:

    The key is the same BASE64_ENCRYPTION_KEY you started the container with. It is needed because no account exists yet and this page reads and writes every secret the server holds — SMTP password, SMS provider token, OAuth client secrets — so it is never open to an unauthenticated visitor, not even on a fresh install. Once you have created an account and given it the ADMIN role, signing in is enough and the key is no longer required.

    To keep the key out of your browser history and any proxy logs, you can send it as a header instead:

    curl -H "X-Master-Key: <paste-your-base64-key>" http://localhost/admin/config

Tell the server how it is reached

The abuse limits count per client address, and the server can only work out which address that is if it knows what stands between it and the internet. X-Forwarded-For is written by the caller as much as by any proxy, so entries are trustworthy only from the right-hand end inwards — and only as far in as the proxies you actually run. TRUSTED_PROXY_COUNT is how many that is.

Hosting platforms do this in one of two ways, and they need opposite settings.

Some edges publish the client address under a header of their own. Name it and it's used as-is — no counting, nothing of the caller's mixed in. Prefer this wherever it's offered:

Platform Setting
Railway CLIENT_IP_HEADER=X-Real-IP
Fly.io CLIENT_IP_HEADER=Fly-Client-IP
Behind Cloudflare CLIENT_IP_HEADER=CF-Connecting-IP

Other edges append to X-Forwarded-For, leaving whatever the caller sent to the left of their own entries. There, count hops in from the right:

Deployment Setting
Google Cloud Run, at its own run.app URL TRUSTED_PROXY_COUNT=1
Behind a GCP external Application Load Balancer TRUSTED_PROXY_COUNT=2
Your own nginx / Caddy in front TRUSTED_PROXY_COUNT=1, plus one per extra hop
Exposed directly, as in the quickstart above leave both unset, and set FORWARD_HEADERS_STRATEGY=none

Count only proxies you control. Guessing too high is the safe direction — the server falls back to the connection's own peer address. Guessing too low attributes every request to your proxy, so one shared budget covers all your users and the limits refuse them together.

Getting this wrong is not cosmetic: trust a header the edge does not overwrite and callers simply nominate who gets counted, so the limits stop working while still appearing to be on. The server logs a warning naming the relevant setting when it can tell something is off, but it cannot detect every case.

Checking it, rather than trusting the table

Providers change, they disagree with their own documentation, and putting a CDN in front changes the answer again. Once deployed, sign in as an admin and call:

curl https://your-auth-server/admin/client-ip -H "Cookie: <your session>"

It reports the address the limits are currently counting you as, how that was decided, and every client-address header the request actually carried. Call it from a phone on mobile data — somewhere the public address is unmistakably yours — and set CLIENT_IP_HEADER to whichever header came back holding it. If instead X-Forwarded-For ends with your address, count its position from the right and use TRUSTED_PROXY_COUNT.

The last row is the only one that should turn off FORWARD_HEADERS_STRATEGY. Everywhere else it must stay at its default of framework, because that is what tells the server it was reached over HTTPS — without it, session cookies lose Secure, cross-site sign-in stops working, and verification emails carry http:// links.

Cloud Run
gcloud run deploy komposeauth \
  --image pitampoudel/komposeauth:latest \
  --set-env-vars MONGODB_URI="mongodb+srv://...",BASE64_ENCRYPTION_KEY="<your-base64-key>",TRUSTED_PROXY_COUNT=1 \
  --min-instances 0 \
  --concurrency 40 \
  --cpu 1 --memory 1Gi \
  --cpu-boost \
  --startup-probe httpGet.path=/actuator/health/readiness,httpGet.port=8080,initialDelaySeconds=4,periodSeconds=2,timeoutSeconds=2,failureThreshold=45

scripts/deploy-cloud-run.sh does all of this from deploy-targets.json, which is worth using once you have more than one target.

TRUSTED_PROXY_COUNT=1 is what Cloud Run needs: its front end appends the caller's address as the last X-Forwarded-For entry, which is the one this server reads, and sets X-Forwarded-Proto: https for the default framework strategy to pick up. Use 2 instead if you front the service with an external Application Load Balancer, which appends both the client address and its own forwarding rule.

Scaling to several instances is already accounted for — sessions, OAuth2 authorizations and the abuse counters all live in MongoDB rather than in one container's memory, so limits hold across instances and survive cold starts.

2) Add the SDK to your KMP project

Shared module (optional and also included already on client module)

// Check the badge above for the latest version
implementation("io.github.pitampoudel:komposeauth-shared:x.x.x")

Client module

// Check the badge above for the latest version
implementation("io.github.pitampoudel:komposeauth-client:x.x.x")

HttpClient example (at each platform)

val httpClient = HttpClient {
    installKomposeAuth(
        authServerUrl = "https://your-auth-server",
        resourceServerUrls = listOf(
            "https://your-resource-server"
        )
    )
}

Initialize SDK

initializeKomposeAuth(
    httpClient = httpClient
)

Integrating with your apps (guide for LLMs and developers)

A complete integration guide, written so an LLM coding assistant can follow it, covers frontend sign-in (OAuth 2.1 authorization code + PKCE for SPAs, server-rendered and mobile apps, or the first-party /login API), the token claims, and resource-server token validation for Spring Boot, Node.js, Python, Go and Ktor.

Every running server also publishes both, without authentication, at https://your-auth-server/llms.txt and https://your-auth-server/llms-full.txt. Point your assistant at one of those URLs.

Usage snippets (Client)

Utilities

  • ScreenStateWrapper(...) with InfoDialog and Progress dialog
  • CountryPicker(...), DateTimeField(...), OTPTextField(...)
  • rememberFilePicker(input, selectionMode, onPicked)
  • rememberKmpCredentialManager()
  • registerSmsOtpRetriever(onRetrieved)
  • (ENUM, GeneralValidationError).toStringRes()

Current user

val userState = rememberCurrentUser()

Login with Credential Manager

val vm = koinViewModel<LoginViewModel>()
val state = vm.state.collectAsStateWithLifecycle().value
val credentialManager = rememberKmpCredentialManager()
LaunchedEffect(state.loginConfig) {
    state.loginConfig?.let {
        when (val result = credentialManager.getCredential(it)) {
            is Result.Error -> vm.onEvent(LoginEvent.ShowInfoMsg(result.message))
            is Result.Success<Credential> -> vm.onEvent(LoginEvent.Login(result.data))
        }
    }
}

OTP

val vm = koinViewModel<OtpViewModel>()
registerSmsOtpRetriever { code ->
    // vm.onEvent(OtpEvent.CodeChanged(code))
}

Profiles and KYC

val profileVm = koinViewModel<ProfileViewModel>()
val kycVm = koinViewModel<KycViewModel>()

Contributing

  • Issues and PRs are welcome
  • Please run ./gradlew build before submitting a PR
  • For larger changes, consider opening an issue first to discuss direction

Security

Cross-site requests

There are no CSRF tokens. Instead, the browser's Sec-Fetch-Site header decides:

  • Same-origin requests (this server's own pages, including the admin console) are always allowed, whatever proxy sits in front of the server.
  • Cross-site requests (anything a browser sends from another site, including a plain form post) are allowed only from origins listed under CORS allowed origins on the configuration page. With the list empty, no other site can send a cookie-authenticated write to this server.
  • Requests from outside a browser (the KMP SDK, mobile apps, backends) do not carry the header and are unaffected. Anything sending Authorization: Bearer is not forgeable anyway.

So a browser app on another origin that calls this server with credentials: "include" only needs its origin in that list.

Reporting

If you discover a security vulnerability, please email the maintainers or open a private security advisory. Avoid filing public issues with sensitive details.

License

Apache License 2.0. See LICENSE for details.