365 líneas
9.8 KiB
Markdown
365 líneas
9.8 KiB
Markdown
# 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
|