Skip to content
11 changes: 8 additions & 3 deletions authorizer-app-backend/authorizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ restSourceClients:
clientId: <CLIENT_ID>
clientSecret: <CLIENT_SECRET>
scope: activity heartrate sleep profile
usesPkce: false
# Garmin OAuth2 PKCE configuration.
# oauthVersion: "oauth2" selects the GarminOAuth2AuthorizationService;
# "oauth1" selects the legacy GarminOAuth1AuthorizationService.
# usesPkce: must be true for Garmin OAuth2 (Garmin requires PKCE).
- sourceType: Garmin
authorizationEndpoint: https://connect.garmin.com/oauth2Confirm
tokenEndpoint: https://diauth.garmin.com/di-oauth2-service/oauth/token
deregistrationEndpoint: https://apis.garmin.com/wellness-api/rest/user/registration
clientId: <GARMIN_CLIENT_ID>
clientSecret: <GARMIN_CLIENT_SECRET>
oauthVersion: oauth2
usesPkce: true
# Google Health API
- sourceType: Google
authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth
tokenEndpoint: https://oauth2.googleapis.com/token
clientId: <GOOGLE_CLIENT_ID>
clientSecret: <GOOGLE_CLIENT_SECRET>
scope: https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly https://www.googleapis.com/auth/googlehealth.sleep.readonly https://www.googleapis.com/auth/googlehealth.profile.readonly https://www.googleapis.com/auth/googlehealth.nutrition.readonly https://www.googleapis.com/auth/googlehealth.irn.readonly https://www.googleapis.com/auth/googlehealth.ecg.readonly
oauthVersion: oauth2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this required config? should it not be oauth2 by default?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is OAuth 2.0 by default. I only kept it here to highlight it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likely remove it if that is the default and does not require configuration

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I do it now, or after the Garmin migration? We might be able to remove this configuration completely then, since we'll only be left with oauth 2.0.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes i think for garmin, it should still be configurable if needed (especially for legacy deployments and until Garmin forces use of OAuth2.0)

Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ data class RestOauth2UserId(
val userId: String,
)

@Serializable
data class RestGoogleHealthIdentity(
val healthUserId: String,
val legacyUserId: String? = null,
)

@Serializable
data class OuraAuthUserId(
val age: Int? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl
import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService
import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.FITBIT_AUTH
import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GARMIN_AUTH
import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GOOGLE_AUTH
import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.OURA_AUTH
import org.radarbase.authorizer.service.GarminOAuth2AuthorizationService
import org.radarbase.authorizer.service.GarminOauth1AuthorizationService
import org.radarbase.authorizer.service.GoogleHealthAuthorizationService
import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService
import org.radarbase.authorizer.service.OuraAuthorizationService
import org.radarbase.authorizer.service.RegistrationService
Expand All @@ -46,21 +48,21 @@ class AuthorizerResourceEnhancer(
private val restSourceClients = RestSourceClients(
config.restSourceClients
.map { it.withEnv() }
.map {
when {
it.sourceType == GARMIN_AUTH && it.oauthVersion.equals("oauth2", ignoreCase = true) ->
it.copy(usesPkce = true)
it.sourceType == GOOGLE_AUTH ->
it.copy(usesPkce = true)
else -> it
}
}
.onEach {
requireNotNull(it.clientId) { "Client ID of ${it.sourceType} is missing" }
requireNotNull(it.clientSecret) { "Client secret of ${it.sourceType} is missing" }
},
)

/**
* Maps a source type to its configured OAuth version (e.g., "oauth1" or "oauth2").
* This is used to conditionally bind the correct authorization service implementation.
* Configure via the `oauthVersion` field in `authorizer.yml` under each `restSourceClients` entry.
*/
private val sourceTypeOauthMap: Map<String, String> = config.restSourceClients.associate {
it.sourceType to it.oauthVersion.lowercase()
}

override val classes: Array<Class<*>>
get() = listOfNotNull(
Filters.cache,
Expand Down Expand Up @@ -112,8 +114,9 @@ class AuthorizerResourceEnhancer(
bind(DelegatedRestSourceAuthorizationService::class.java)
.to(RestSourceAuthorizationService::class.java)

// Bind Garmin service based on a configured oauthVersion: "oauth2" → PKCE flow, "oauth1" → legacy flow.
if (sourceTypeOauthMap[GARMIN_AUTH].equals("oauth2", ignoreCase = true)) {
// Bind Garmin service based on configured oauthVersion: "oauth2" → PKCE flow, "oauth1" → legacy flow.
val garminUsesPkce = restSourceClients.clients.firstOrNull { it.sourceType == GARMIN_AUTH }?.usesPkce == true
if (garminUsesPkce) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think more readable to just use oauthVersion from config here

@this-Aditya this-Aditya Jun 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this in PR #333. As this PR derives usesPkce from the RestSourceClient, I think that's a better place to handle it than doing it here.

bind(GarminOAuth2AuthorizationService::class.java)
.to(RestSourceAuthorizationService::class.java)
.named(GARMIN_AUTH)
Expand All @@ -134,5 +137,10 @@ class AuthorizerResourceEnhancer(
.to(RestSourceAuthorizationService::class.java)
.named(OURA_AUTH)
.`in`(Singleton::class.java)

bind(GoogleHealthAuthorizationService::class.java)
.to(RestSourceAuthorizationService::class.java)
.named(GOOGLE_AUTH)
.`in`(Singleton::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ class DelegatedRestSourceAuthorizationService(
const val GARMIN_AUTH = "Garmin"
const val FITBIT_AUTH = "FitBit"
const val OURA_AUTH = "Oura"
const val GOOGLE_AUTH = "GoogleHealth"
}
}
Loading
Loading