Files
MyActivityPub/docs/OAUTH_LOGIN.md
2026-01-24 17:45:29 +01:00

9.8 KiB

OAuth Login and Authentication

This document explains the OAuth authentication implementation in My ActivityPub app.

Overview

The app supports two modes:

  1. Guest Mode (Default): Browse public federated timeline without authentication
  2. Authenticated Mode: Login with OAuth to access your home timeline and instance

Features

Guest Mode

  • Opens directly to the public federated timeline from mastodon.social
  • No authentication required
  • Browse posts from the entire fediverse
  • Option to login at any time

Authenticated Mode

When logged in, users can:

  • Access their home timeline (posts from followed accounts)
  • Switch between Public and Home timelines
  • See their username in the app header
  • Logout when desired
  • Persistent login (saved across app restarts)

OAuth Flow

1. App Registration

When a user initiates login:

  1. User enters their instance domain (e.g., mastodon.online)
  2. App registers itself with the instance via /api/v1/apps
  3. Receives client_id and client_secret
  4. Credentials are saved securely in DataStore

2. Authorization

  1. App constructs authorization URL:
    https://{instance}/oauth/authorize?
      client_id={client_id}&
      scope=read write follow&
      redirect_uri=myactivitypub://oauth&
      response_type=code
    
  2. Opens URL in Custom Chrome Tab (in-app browser)
  3. User authorizes the app on their instance
  4. Instance redirects to: myactivitypub://oauth?code={auth_code}
  5. App catches the deep link and extracts the code

3. Token Exchange

  1. App exchanges authorization code for access token
  2. POST to /oauth/token with:
    • client_id
    • client_secret
    • code (from authorization)
    • grant_type=authorization_code
  3. Receives access_token
  4. Token is saved securely in DataStore

4. Authenticated Requests

All subsequent API requests include:

Authorization: Bearer {access_token}

Architecture

Components

AuthRepository

Location: data/repository/AuthRepository.kt Handles all authentication operations:

  • registerApp(): Register OAuth app with instance
  • getAuthorizationUrl(): Generate OAuth URL
  • obtainToken(): Exchange code for token
  • verifyCredentials(): Verify user account
  • logout(): Clear stored credentials
  • userSession: Flow of current session state

AuthViewModel

Location: ui/viewmodel/AuthViewModel.kt Manages authentication state:

sealed class AuthState {
    object Loading
    object NotAuthenticated
    object LoggingIn
    data class NeedsAuthorization(val authUrl: String)
    object LoginComplete
    data class Authenticated(val session: UserSession)
    data class Error(val message: String)
}

LoginScreen

Location: ui/screens/LoginScreen.kt UI for login:

  • Instance domain input
  • Login button
  • Continue as guest option
  • Error display

Data Models

UserSession

data class UserSession(
    val instance: String,      // e.g., "mastodon.social"
    val accessToken: String,   // OAuth access token
    val account: Account?      // User account info
)

AppRegistration

data class AppRegistration(
    val id: String,
    val name: String,
    val clientId: String,
    val clientSecret: String,
    val redirectUri: String
)

TokenResponse

data class TokenResponse(
    val accessToken: String,
    val tokenType: String,     // Usually "Bearer"
    val scope: String,         // "read write follow"
    val createdAt: Long
)

Security

Token Storage

  • Tokens stored using DataStore Preferences
  • Encrypted at rest by Android system
  • Not accessible to other apps
  • Cleared on logout

OAuth Best Practices

Implemented:

  • Authorization Code Flow (most secure)
  • Custom Chrome Tabs (prevents phishing)
  • HTTPS only
  • State parameter handling
  • Token refresh (TODO) Not Implemented (Future):
  • PKCE (Proof Key for Code Exchange)
  • Token refresh
  • Token expiration handling
  • Revocation on logout

API Endpoints

Register App

POST /api/v1/apps

Request:

client_name=My ActivityPub
redirect_uris=myactivitypub://oauth
scopes=read write follow

Response:

{
  "id": "123",
  "name": "My ActivityPub",
  "client_id": "...",
  "client_secret": "...",
  "redirect_uri": "myactivitypub://oauth"
}

Obtain Token

POST /oauth/token

Request:

client_id={client_id}
client_secret={client_secret}
redirect_uri=myactivitypub://oauth
grant_type=authorization_code
code={authorization_code}
scope=read write follow

Response:

{
  "access_token": "...",
  "token_type": "Bearer",
  "scope": "read write follow",
  "created_at": 1234567890
}

Verify Credentials

GET /api/v1/accounts/verify_credentials
Authorization: Bearer {access_token}

Response:

{
  "id": "123",
  "username": "alice",
  "display_name": "Alice",
  "avatar": "https://...",
  ...
}

Configuration

OAuth Scopes

Currently requested scopes:

  • read: Read user data
  • write: Post statuses, follow accounts
  • follow: Follow/unfollow accounts

Redirect URI

Fixed redirect URI:

myactivitypub://oauth

Registered in AndroidManifest.xml:

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="myactivitypub"
        android:host="oauth" />
</intent-filter>

Usage Examples

Login Flow (User Perspective)

  1. Open app
    • See public timeline (guest mode)
  2. Tap menu (⋮) → Login
    • Enter instance: mastodon.social
    • Tap "Login" button
  3. Authorize in browser
    • Browser opens showing Mastodon login
    • Enter credentials (if not logged in)
    • Click "Authorize"
  4. Return to app
    • Automatically redirects back
    • Shows "Loading..."
    • Displays home timeline
  5. Use app
    • See home timeline (followed accounts)
    • Tap menu → Switch to Public
    • Tap menu → Logout (when done)

Programmatic Usage

// In a Composable
val authViewModel: AuthViewModel = viewModel()
val userSession by authViewModel.userSession.collectAsState()
// Start login
authViewModel.startLogin("mastodon.social")
// Complete login (after OAuth callback)
authViewModel.completeLogin(authorizationCode)
// Logout
authViewModel.logout()
// Check if logged in
if (userSession != null) {
    // User is authenticated
    val username = userSession.account?.username
}

Troubleshooting

Common Issues

Login button doesn't work

Symptoms: Nothing happens when tapping Login Causes:

  • Invalid instance domain
  • Network connectivity issues
  • Instance doesn't support OAuth Solutions:
  1. Check internet connection
  2. Verify instance domain (no https://, just domain)
  3. Try a known instance like mastodon.social

Browser doesn't open

Symptoms: No browser appears after tapping Login Causes:

  • Chrome not installed
  • Intent filter not configured Solutions:
  1. Install Chrome or another browser
  2. Check AndroidManifest.xml has intent filter

Can't return to app after authorization

Symptoms: Stuck in browser after authorizing Causes:

  • Deep link not registered
  • Activity launch mode incorrect Solutions:
  1. Verify launchMode="singleTask" in manifest
  2. Check intent filter configuration
  3. Manually return to app

Token errors

Symptoms: "Failed to obtain token" error Causes:

  • Invalid authorization code
  • Expired code
  • Network issues Solutions:
  1. Try logging in again
  2. Check network connection
  3. Clear app data and retry

Debug Mode

To enable detailed logging:

val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = HttpLoggingInterceptor.Level.BODY  // Change from BASIC
}

View logs:

adb logcat | grep -E "(OAuth|Auth|Token)"

Future Enhancements

Planned Features

  • Token refresh mechanism
  • PKCE support
  • Multiple account support
  • Account switching
  • Remember last used instance
  • Instance suggestions/autocomplete
  • Token revocation on logout
  • Offline mode with cached data
  • Biometric authentication
  • Session timeout

Security Improvements

  • Implement PKCE
  • Add state parameter validation
  • Token rotation
  • Secure key storage (Android Keystore)
  • Certificate pinning

Testing

Manual Testing

  1. Guest Mode:
    • Open app → Should show public timeline
    • No login required
  2. Login Flow:
    • Tap menu → Login
    • Enter mastodon.social
    • Should open browser
    • Authorize app
    • Should return to app automatically
    • Should show home timeline
  3. Timeline Switching:
    • Login first
    • Tap menu → Switch to Public
    • Should show public timeline
    • Tap menu → Switch to Home
    • Should show home timeline
  4. Logout:
    • While logged in
    • Tap menu → Logout
    • Should return to public timeline (guest mode)

Automated Testing

@Test
fun authRepository_registerApp_success() = runTest {
    val result = authRepository.registerApp("mastodon.social")
    assertTrue(result.isSuccess)
}
@Test
fun authViewModel_login_updatesState() = runTest {
    authViewModel.startLogin("mastodon.social")
    // Verify state changes to LoggingIn, then NeedsAuthorization
}

References

Support

For issues related to authentication:

  1. Check this documentation
  2. Review the code in AuthRepository.kt
  3. Check LogCat for errors
  4. Open an issue on GitHub with:
    • Instance you're trying to connect to
    • Error messages
    • Steps to reproduce