9.8 KiB
OAuth Login and Authentication
This document explains the OAuth authentication implementation in My ActivityPub app.
Overview
The app supports two modes:
- Guest Mode (Default): Browse public federated timeline without authentication
- 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:
- User enters their instance domain (e.g.,
mastodon.online) - App registers itself with the instance via
/api/v1/apps - Receives
client_idandclient_secret - Credentials are saved securely in DataStore
2. Authorization
- App constructs authorization URL:
https://{instance}/oauth/authorize? client_id={client_id}& scope=read write follow& redirect_uri=myactivitypub://oauth& response_type=code - Opens URL in Custom Chrome Tab (in-app browser)
- User authorizes the app on their instance
- Instance redirects to:
myactivitypub://oauth?code={auth_code} - App catches the deep link and extracts the code
3. Token Exchange
- App exchanges authorization code for access token
- POST to
/oauth/tokenwith:client_idclient_secretcode(from authorization)grant_type=authorization_code
- Receives
access_token - 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 instancegetAuthorizationUrl(): Generate OAuth URLobtainToken(): Exchange code for tokenverifyCredentials(): Verify user accountlogout(): Clear stored credentialsuserSession: 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 datawrite: Post statuses, follow accountsfollow: 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)
- Open app
- See public timeline (guest mode)
- Tap menu (⋮) → Login
- Enter instance:
mastodon.social - Tap "Login" button
- Enter instance:
- Authorize in browser
- Browser opens showing Mastodon login
- Enter credentials (if not logged in)
- Click "Authorize"
- Return to app
- Automatically redirects back
- Shows "Loading..."
- Displays home timeline
- 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:
- Check internet connection
- Verify instance domain (no
https://, just domain) - 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:
- Install Chrome or another browser
- Check
AndroidManifest.xmlhas 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:
- Verify
launchMode="singleTask"in manifest - Check intent filter configuration
- Manually return to app
Token errors
Symptoms: "Failed to obtain token" error Causes:
- Invalid authorization code
- Expired code
- Network issues Solutions:
- Try logging in again
- Check network connection
- 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
- Guest Mode:
- Open app → Should show public timeline
- No login required
- Login Flow:
- Tap menu → Login
- Enter
mastodon.social - Should open browser
- Authorize app
- Should return to app automatically
- Should show home timeline
- Timeline Switching:
- Login first
- Tap menu → Switch to Public
- Should show public timeline
- Tap menu → Switch to Home
- Should show home timeline
- 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:
- Check this documentation
- Review the code in
AuthRepository.kt - Check LogCat for errors
- Open an issue on GitHub with:
- Instance you're trying to connect to
- Error messages
- Steps to reproduce