Skip to main content
Version: Next

RADAR-Rest-Source-Auth — Architecture

Overview

Kotlin/JAX-RS via Jersey backend that manages OAuth consent and token lifecycle for third-party wearable/health APIs. Stores tokens in PostgreSQL; uses Redis for distributed locking during concurrent refresh.


Directory Structure

RADAR-Rest-Source-Auth/
├── authorizer-app-backend/ # Main application module
│ ├── src/main/java/org/radarbase/authorizer/
│ │ ├── Main.kt # Entry point — loads config, starts Grizzly/Jersey
│ │ ├── api/ # Request/response DTOs
│ │ ├── config/ # Configuration data classes
│ │ ├── doa/ # Hibernate repositories + JPA entities
│ │ ├── enhancer/ # HK2 DI binders and Jersey setup
│ │ ├── lifecycle/ # Startup/shutdown hooks (registration cleanup)
│ │ ├── resources/ # JAX-RS REST endpoints (controllers)
│ │ ├── service/ # Business logic and auth implementations
│ │ └── util/ # HMAC-256, OAuth1 signing utilities
│ ├── src/main/resources/db/ # Liquibase migrations
│ └── authorizer.yml # Runtime configuration template
├── buildSrc/ # Custom Gradle plugins
├── docker/ # Docker configs and compose files
└── docs/ # Architecture and reference docs

Key Packages

PackagePurpose
apiDTOs: RestOauth2AccessToken, RequestTokenPayload, RestSourceUserDTO, etc.
configAuthorizerConfig (root), AuthorizerServiceConfig, RestSourceClient, RedisConfig
doaRepositories (RestSourceUserRepository, RegistrationRepository) and entities
doa.entityRestSourceUser, RegistrationState — JPA entities backed by PostgreSQL
enhancerAuthorizerResourceEnhancer — HK2 bindings for all services
resourcesRegistrationResource, RestSourceUserResource, SourceClientResource, ProjectResource
serviceAuthorization services (OAuth2/OAuth1/Huawei/Garmin/Oura), user and client services

REST API

/registrations — OAuth flow initiation

MethodPathAction
POST/registrationsCreate ephemeral state token for a user
GET/registrations/{token}Fetch registration details
POST/registrations/{token}Get OAuth authorize URL (validates HMAC secret)
POST/registrations/{token}/authorizeExchange auth code for tokens
DELETE/registrations/{token}Cancel registration

/users — User account management

MethodPathAction
GET/POST/usersList / create users
GET/POST/DELETE/users/{id}Get / update / delete user
POST/users/{id}/resetReset authorization
GET/POST/users/{id}/tokenCheck / refresh token

/source-clients — OAuth client configuration

MethodPathAction
GET/source-clientsList all configured sources
GET/source-clients/{type}Get config for a source type
POST/source-clients/{type}/deregisterWebhook for provider-initiated deregistration

Authorization Service Architecture

All authorization implementations share the RestSourceAuthorizationService interface. DelegatedRestSourceAuthorizationService routes calls to the named implementation by sourceType.

RestSourceAuthorizationService (interface)
├── OAuth2RestSourceAuthorizationService → FitBit (default OAuth2 + Basic Auth)
│ ├── OuraAuthorizationService → Oura (+ custom user ID fetch, custom revoke)
│ └── HuaweiAuthorizationService → Huawei (+ form-param auth, JWT id_token parsing)
└── OAuth1RestSourceAuthorizationService
└── GarminSourceAuthorizationService → Garmin (+ user ID API call, deregistration scheduler)

Adding a New Source

  1. Create XyzAuthorizationService extending the appropriate base.
  2. Add const val XYZ_AUTH = "Xyz" to DelegatedRestSourceAuthorizationService.Companion.
  3. Bind in AuthorizerResourceEnhancer.enhance() with .named(XYZ_AUTH).
  4. Add the source client block to authorizer.yml.

Database

Tables

rest_source_user — One row per authorized user per source type.

ColumnTypeNotes
idbigint PK
project_id, user_idvarcharRADAR identifiers
source_idUUIDKafka record key (unique)
source_typevarchar"FitBit", "Garmin", "Oura", "Huawei", …
external_user_idvarcharProvider's user ID
authorizedbooleanCurrent auth status
access_tokenvarchar(2000)
refresh_tokenvarchar(2000)
expires_attimestampComputed from expires_in
start_date, end_datetimestampData collection window
version, times_resetintReset tracking

registration — Short-lived state tokens for the OAuth flow.

ColumnTypeNotes
tokenvarchar PKState param in OAuth URL
user_idFK → rest_source_user
salt, secret_hashbyteaHMAC-256 for persistent tokens
created_at, expires_attimestampTTL
persistentbooleanLong-lived vs ephemeral

Migrations managed by Liquibase under src/main/resources/db/changelog/.


Configuration

authorizer.yml (parsed into AuthorizerConfig):

service:
baseUri: http://0.0.0.0:8085/rest-sources/backend/
advertisedBaseUri: http://example.org/rest-sources/backend/
# callbackUrl derived from advertisedBaseUri or frontendBaseUri

auth:
managementPortalUrl: https://...
clientId: radar_rest_sources_auth
clientSecret: <secret>

database:
driver: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/managementportal
user: radar
password: radar_test

redis:
uri: redis://localhost:6379
lockPrefix: radar-rest-sources-backend/lock

restSourceClients:
- sourceType: FitBit
...

Secrets can be overridden via env vars: {SOURCETYPE}_CLIENT_ID, {SOURCETYPE}_CLIENT_SECRET.


Dependency Injection

Framework: HK2 (Jersey's DI). All bindings in AuthorizerResourceEnhancer.enhance().

  • Services bound as singletons.
  • DelegatedRestSourceAuthorizationService receives an IterableProvider<RestSourceAuthorizationService> and dispatches by the HK2 named binding that matches the sourceType string.

Key Dependencies

LibraryPurpose
radar-jerseyJAX-RS + Jersey + Hibernate integration
ktor-clientAsync HTTP for token exchange calls
kotlinx.serializationJSON (de)serialization for API responses
postgresql / HibernateORM + DB
JedisRedis client for distributed token-refresh locking
LiquibaseDB migrations