Skip to content

wimdetroyer/strava-spring-boot-starter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Strava Spring Boot Starter

License Java 25

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.

Requirements

  • Java 25 or newer
  • Spring Boot 4.0 or newer

OpenAPI Specification

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, and max_heartrate fields to SummaryActivity
  • Other fields returned by the Strava API but missing from the official spec may be added as needed

Features

  • 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)

Getting Started

Prerequisites

Before using this library, an athlete needs to create a Strava API application:

  1. Go to Strava API Settings
  2. 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., localhost for 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!)

Installation

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'

Configuration

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);

Auto-Configured Beans

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

OAuth Integration Flow

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)    │
     │  }                                                           │
     └──────────────────────────────────────────────────────────────┘

Code Example

// 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 via StravaOAuthService.refreshToken() when needed. Again, your responsibility.

Integration Example

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");
    }
}

Using the API Clients

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

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.

BaseStravaClient (Direct API Access)

@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
        );
    }
}

FluentStravaClient (Builder Pattern API)

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();

Testing

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.

StravaTokens

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.

Available Scopes

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

API Endpoints

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

Disclaimer

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.

License

Apache License 2.0

About

Java Spring boot RestClient starter for the Strava API, with OAUTH utilities and a more readable, opinionated fluent API

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages