A Spring Boot starter for integrating with the Strava API. This library provides OAuth 2.0 authentication flow handling and type-safe API clients generated from the official Strava OpenAPI specification.
- Java 25 or newer
- Spring Boot 4.0 or newer
The API clients are generated from the official Strava OpenAPI specification using OpenAPI Generator. The spec (strava-api.json) has been locally modified during development because the official spec is incomplete:
- Added
has_heartrate,average_heartrate, andmax_heartratefields toSummaryActivity - Other fields returned by the Strava API but missing from the official spec may be added as needed
- OAuth 2.0 Integration - Complete OAuth flow handling including authorization URL generation, token exchange, and token refresh
- Per-credential OAuth - All OAuth methods take explicit
StravaCredentials, supporting multi-athlete scenarios - BaseStravaClient - Base API client facade providing 'raw' access to all Strava API endpoints
- FluentStravaClient - Builder-pattern API client for more readable, chainable API calls
- Spring Boot Auto-Configuration - Automatic bean configuration with minimal setup
- Generated from OpenAPI - API clients generated from official Strava API specification (with local additions)
Before using this library, an athlete needs to create a Strava API application:
- Go to Strava API Settings
- Create a new application with the following information:
- Application Name: Your application's name
- Category: Select the appropriate category for your app
- Website: Your application's website URL
- Authorization Callback Domain: The domain where Strava will redirect after authorization (e.g.,
localhostfor development)
Note: The Authorization Callback Domain must match the host in your redirect URI. For local development, use
localhost.
After creating your application, you'll receive:
- Client ID: Your application's unique identifier
- Client Secret: Your application's secret key (keep this secure!)
Add the dependency to your project:
Maven:
<dependency>
<groupId>com.wimdetroyer</groupId>
<artifactId>strava-spring-boot-starter</artifactId>
<version>LATEST_VERSION</version>
</dependency>Gradle:
implementation 'com.wimdetroyer:strava-spring-boot-starter:LATEST_VERSION'No global configuration is required. The starter auto-configures StravaOAuthService and StravaClientFactory beans.
You provide credentials per call. An example flow:
// Build credentials from wherever you store them (config, database, user profile, etc.)
StravaCredentials credentials = new StravaCredentials(clientId, clientSecret, redirectUri);
// Use credentials for OAuth operations
String authUrl = stravaOAuthService.getAuthorizationUrl(credentials, scopes, state);
StravaTokens tokens = stravaOAuthService.exchangeCodeForTokens(credentials, code);
StravaTokens refreshed = stravaOAuthService.refreshToken(credentials, oldRefreshToken);The starter automatically configures the following beans:
| Bean | Description |
|---|---|
StravaOAuthService |
Stateless OAuth 2.0 flow handler — takes StravaCredentials per call |
StravaClientFactory |
Factory for creating authenticated API clients |
The OAuth 2.0 authorization flow works as follows:
┌──────────┐ 1. Generate Auth URL ┌─────────────────────┐
│ Your │ ────────────────────────────► │ StravaOAuthService │
│ App │ ◄──────────────────────────── │ (+ credentials) │
└──────────┘ Authorization URL └─────────────────────┘
│
│ 2. Redirect user to Strava
▼
┌──────────┐
│ Strava │ User authorizes your app
└──────────┘
│
│ 3. Redirect to your callback with code
▼
┌──────────┐ 4. Exchange code ┌─────────────────────┐
│ Your │ ────────────────────────────► │ StravaOAuthService │
│ App │ ◄──────────────────────────── │ (+ credentials) │
└──────────┘ StravaTokens └─────────────────────┘
│
│ 5. Store tokens (your implementation)
▼
┌──────────┐
│ Your │
│ Storage │
└──────────┘
│
│ 6. Create client & make API calls
▼
┌──────────┐ createClient/ ┌─────────────────────┐
│ Your │ createFluentClient │ StravaClientFactory │
│ App │ ────────────────────────────► │ │
└──────────┘ (accessToken) └─────────────────────┘
│
▼
┌──────────┐ ┌─────────────────────┐
│ Strava │ ◄──────────────────────────── │ BaseStravaClient │
│ API │ API requests with token │ FluentStravaClient │
└──────────┘ └─────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 7. Token Refresh (when expired) │
│ │
│ if (tokens.isExpired()) { │
│ newTokens = oAuthService.refreshToken(creds, refresh) │
│ store(newTokens) │
│ client = factory.createClient(newTokens.accessToken) │
│ } │
└──────────────────────────────────────────────────────────────┘
// Credentials can come from config, database, user input, etc.
StravaCredentials credentials = new StravaCredentials(clientId, clientSecret, redirectUri);
// Step 1: Generate authorization URL and redirect user
String authUrl = stravaOAuthService.getAuthorizationUrl(
credentials,
Set.of(StravaScope.READ, StravaScope.ACTIVITY_READ_ALL),
"csrf-state-token"
);
// Redirect user to authUrl...
// Step 2: Handle callback and exchange code for tokens
StravaTokens tokens = stravaOAuthService.exchangeCodeForTokens(credentials, authorizationCode);
// Step 3: Store tokens (your responsibility)
myTokenStore.save(userId, tokens);
// Step 4: Use access token for API calls
FluentStravaClient client = stravaClientFactory.createFluentClient(tokens.accessToken());
// Step 5: Refresh when expired
if (tokens.isExpired()) {
StravaTokens newTokens = stravaOAuthService.refreshToken(credentials, tokens.refreshToken());
myTokenStore.save(userId, newTokens);
}Note: Token storage is intentionally left to the consuming application. This allows flexibility in how tokens are persisted.
Token Expiry: Strava access tokens expire after 6 hours. Use
StravaTokens.isExpired()to check validity and refresh viaStravaOAuthService.refreshToken()when needed. Again, your responsibility.
Here's a complete example of implementing the OAuth flow with a controller and service:
@Controller
@RequiredArgsConstructor
public class StravaAuthController {
private final StravaOAuthService stravaOAuthService;
private final StravaService stravaService;
/**
* Initiate OAuth flow - redirect user to Strava for authorization.
* The 'state' parameter is your app's user ID (not the Strava athlete ID).
*/
@GetMapping("/{userId}/strava/connect")
public String connect(@PathVariable String userId) {
StravaCredentials credentials = stravaService.getCredentials(userId);
String authUrl = stravaOAuthService.getAuthorizationUrl(
credentials,
Set.of(StravaScope.READ, StravaScope.ACTIVITY_READ_ALL),
userId
);
return "redirect:" + authUrl;
}
/**
* Handle OAuth callback from Strava. (registered)
*/
@GetMapping("/strava/callback")
public String callback(
@RequestParam String code,
@RequestParam(required = false) String state) {
StravaCredentials credentials = stravaService.getCredentials(state);
StravaTokens tokens = stravaOAuthService.exchangeCodeForTokens(credentials, code);
stravaService.saveTokens(state, tokens);
return "redirect:/profile/" + state + "?connected=true";
}
}
@Service
@RequiredArgsConstructor
public class StravaService {
private final StravaOAuthService stravaOAuthService;
private final StravaClientFactory stravaClientFactory;
/**
* Get credentials for a user. Could come from database, user profile, config, etc.
*/
public StravaCredentials getCredentials(String userId) {
// Your logic to load per-user Strava API credentials here...
}
public void saveTokens(String userId, StravaTokens tokens) {
// Your persistence logic here...
}
/**
* Retrieve tokens, refreshing if expired.
* The Supplier pattern allows the client to auto-refresh tokens.
* example below...
*/
public Supplier<String> getAccessTokenSupplier(String userId) {
return () -> {
StravaTokens tokens = loadTokens(userId);
if (tokens.isExpired()) {
StravaCredentials credentials = getCredentials(userId);
tokens = stravaOAuthService.refreshToken(credentials, tokens.refreshToken());
saveTokens(userId, tokens);
}
return tokens.accessToken();
};
}
private StravaTokens loadTokens(String userId) {
throw new UnsupportedOperationException("Implement your token loading");
}
}The StravaClientFactory provides two client variants, each with static token or dynamic supplier options:
| Method | Returns | Description |
|---|---|---|
createClient(accessToken) |
BaseStravaClient |
Direct access with static token |
createClient(tokenSupplier) |
BaseStravaClient |
Direct access with dynamic token supplier |
createFluentClient(accessToken) |
FluentStravaClient |
Fluent API with static token |
createFluentClient(tokenSupplier) |
FluentStravaClient |
Fluent API with dynamic token supplier |
For long-lived clients, use a Supplier<String> to handle token refresh automatically. The supplier is called on each API request, allowing you to check expiration and refresh as needed:
Again, you're in charge of the implementation here.
StravaCredentials credentials = getCredentials(userId);
FluentStravaClient client = clientFactory.createFluentClient(() -> {
if (tokens.isExpired()) {
tokens = stravaOAuthService.refreshToken(credentials, tokens.refreshToken());
saveToDatabase(tokens);
}
return tokens.accessToken();
});
// Use client - supplier is called on each API request
List<SummaryActivity> activities = client.activities().listMine().execute();This approach keeps the client instance stable while tokens are refreshed transparently.
@Service
@RequiredArgsConstructor
public class ActivityService {
private final StravaClientFactory stravaClientFactory;
private final StravaService stravaService;
public List<SummaryActivity> getRecentActivities(String userId) {
StravaTokens tokens = stravaService.getValidTokens(userId)
.orElseThrow(() -> new IllegalStateException("User not connected to Strava"));
BaseStravaClient client = stravaClientFactory.createClient(tokens.accessToken());
return client.activities().getLoggedInAthleteActivities(
null, // before (epoch timestamp)
null, // after (epoch timestamp)
1, // page
30 // per_page
);
}
}The FluentStravaClient provides a more readable, chainable API (as concocted by claude code and myself):
FluentStravaClient client = stravaClientFactory.createFluentClient(accessToken);
// List activities with default parameters
List<SummaryActivity> activities = client.activities()
.listMine()
.execute();
// List activities with custom pagination
List<SummaryActivity> activities = client.activities()
.listMine()
.page(2)
.perPage(50)
.execute();
// Get a specific activity with all segment efforts
DetailedActivity activity = client.activities()
.get(12345L)
.includeAllEfforts(true)
.execute();
// Get authenticated athlete profile
DetailedAthlete athlete = client.athletes()
.me()
.execute();The project has two layers of tests:
Offline unit tests (run by default via mvn test) cover OAuth URL generation, token response mapping, scope parsing, auto-configuration, and mocked HTTP interactions with the Strava token endpoint. These require no credentials and no network.
Live integration / sandbox tests are gated behind the STRAVA_LIVE_TESTS=true environment variable and hit the real Strava API. To run them, bring your own Strava API credentials by setting the env vars referenced from src/test/resources/application-test.yml:
strava-test:
client-id: ${STRAVA_CLIENT_ID}
client-secret: ${STRAVA_CLIENT_SECRET}
redirect-uri: http://localhost:8080/callback
refresh-token: ${STRAVA_REFRESH_TOKEN}You can either set the environment variables or replace the placeholders with actual values.
The StravaTokens record contains the OAuth tokens returned from Strava:
public record StravaTokens(
String accessToken,
String refreshToken,
Instant expiresAt,
Long athleteId
) {
public boolean isExpired() {
return Instant.now().isAfter(expiresAt);
}
}| Field | Description |
|---|---|
accessToken |
Token used for API requests |
refreshToken |
Token used to obtain new access tokens |
expiresAt |
When the access token expires |
athleteId |
The Strava athlete ID of the authenticated user |
Use isExpired() to check if the token needs to be refreshed before making API calls. A small convenience method.
When building the authorization URL, use the StravaScope enum for type-safe scope selection:
| Enum Value | Scope String | Description |
|---|---|---|
StravaScope.READ |
read |
Read public segments, public routes, public profile data, public posts, public events, club feeds, and leaderboards |
StravaScope.READ_ALL |
read_all |
Read private routes, private segments, and private events for the user |
StravaScope.PROFILE_READ_ALL |
profile:read_all |
Read all profile information even if the user has set their profile visibility to Followers or Only You |
StravaScope.PROFILE_WRITE |
profile:write |
Update the user's weight and FTP, and access to star or unstar segments on their behalf |
StravaScope.ACTIVITY_READ |
activity:read |
Read activity data for activities visible to Everyone and Followers, excluding privacy zone data. Required for activity webhooks. |
StravaScope.ACTIVITY_READ_ALL |
activity:read_all |
Same as activity:read, plus privacy zone data and access to activities with visibility set to Only You |
StravaScope.ACTIVITY_WRITE |
activity:write |
Create manual activities and uploads, and edit any activities visible to the app based on activity read access level |
Both clients provide access to all Strava API endpoints. These are generated from the official Strava OpenAPI specification.
| API | Description |
|---|---|
activities() |
Create, read, update activities |
athletes() |
Read athlete profiles and stats |
clubs() |
Read club information and activities |
gears() |
Read gear/equipment information |
routes() |
Read route information |
segments() |
Read segment information |
segmentEfforts() |
Read segment effort details |
streams() |
Read activity/segment streams (GPS, heart rate, etc.) |
uploads() |
Upload activity files |
I don't provide any guarantees about how well this library functions. I only use a minimal subset of the API in a hobby project. Moreover, this library was largely created by Claude Code. It does work well though in my experience, and I'm sure it'll save you some time to use this.