364
docs/OAUTH_LOGIN.md
Archivo normal
364
docs/OAUTH_LOGIN.md
Archivo normal
@@ -0,0 +1,364 @@
|
||||
# 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
|
||||
<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
|
||||
```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
|
||||
Referencia en una nueva incidencia
Block a user