# 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: ```kotlin 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 ```kotlin data class UserSession( val instance: String, // e.g., "mastodon.social" val accessToken: String, // OAuth access token val account: Account? // User account info ) ``` #### AppRegistration ```kotlin data class AppRegistration( val id: String, val name: String, val clientId: String, val clientSecret: String, val redirectUri: String ) ``` #### TokenResponse ```kotlin 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**: ```json { "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**: ```json { "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**: ```json { "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`: ```xml ``` ## 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 ```kotlin // 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: ```kotlin val loggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY // Change from BASIC } ``` View logs: ```bash 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 ```kotlin @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 - [Mastodon OAuth Documentation](https://docs.joinmastodon.org/spec/oauth/) - [RFC 6749 - OAuth 2.0](https://tools.ietf.org/html/rfc6749) - [Android Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/) - [DataStore Documentation](https://developer.android.com/topic/libraries/architecture/datastore) ## 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