android-sdd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-contract
|
|
3
|
+
description: >
|
|
4
|
+
Defining and consuming API contracts in Android projects.
|
|
5
|
+
Load this skill when documenting API expectations, defining request/response
|
|
6
|
+
DTOs, handling API versioning, managing breaking changes, or aligning
|
|
7
|
+
the data layer with backend contracts.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# API Contract
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
An API contract is the formal agreement between client and server about request/response structure, status codes, error formats, and versioning. A well-defined contract prevents runtime surprises, makes the data layer predictable, and provides a stable foundation for mocking in tests.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Define all request/response shapes as `@Serializable` DTOs — never use `Map<String, Any>`
|
|
20
|
+
- Document the expected status codes for every endpoint
|
|
21
|
+
- Version the API from day one — breaking changes require a new version
|
|
22
|
+
- Use `ignoreUnknownKeys = true` in the Json instance — tolerates additive API changes
|
|
23
|
+
- Never couple domain models to the API contract — always map through DTOs
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Contract Definition
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Request DTOs — what we send
|
|
31
|
+
@Serializable
|
|
32
|
+
data class CreateUserRequest(
|
|
33
|
+
@SerialName("full_name") val name: String,
|
|
34
|
+
@SerialName("email") val email: String,
|
|
35
|
+
@SerialName("password") val password: String
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@Serializable
|
|
39
|
+
data class UpdateUserRequest(
|
|
40
|
+
@SerialName("full_name") val name: String? = null,
|
|
41
|
+
@SerialName("email") val email: String? = null
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// ✅ Response DTOs — what we receive
|
|
45
|
+
@Serializable
|
|
46
|
+
data class UserDto(
|
|
47
|
+
@SerialName("id") val id: String,
|
|
48
|
+
@SerialName("full_name") val name: String,
|
|
49
|
+
@SerialName("email") val email: String,
|
|
50
|
+
@SerialName("avatar_url") val avatarUrl: String? = null,
|
|
51
|
+
@SerialName("created_at") val createdAt: Long,
|
|
52
|
+
@SerialName("is_active") val isActive: Boolean = true
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// ✅ Error response shape
|
|
56
|
+
@Serializable
|
|
57
|
+
data class ApiErrorResponse(
|
|
58
|
+
@SerialName("code") val code: String,
|
|
59
|
+
@SerialName("message") val message: String,
|
|
60
|
+
@SerialName("details") val details: Map<String, List<String>> = emptyMap()
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Endpoint Contract Documentation
|
|
67
|
+
|
|
68
|
+
```kotlin
|
|
69
|
+
// ✅ Document contract as comments on the API interface
|
|
70
|
+
interface UserApi {
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* GET /users/{id}
|
|
74
|
+
*
|
|
75
|
+
* Success: 200 UserDto
|
|
76
|
+
* Not Found: 404 ApiErrorResponse
|
|
77
|
+
* Unauthorized: 401 ApiErrorResponse
|
|
78
|
+
*/
|
|
79
|
+
@GET("users/{id}")
|
|
80
|
+
suspend fun getUser(@Path("id") id: String): UserDto
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* POST /users
|
|
84
|
+
*
|
|
85
|
+
* Success: 201 UserDto
|
|
86
|
+
* Validation Error: 422 ApiErrorResponse (details contains field errors)
|
|
87
|
+
* Conflict: 409 ApiErrorResponse (email already exists)
|
|
88
|
+
*/
|
|
89
|
+
@POST("users")
|
|
90
|
+
suspend fun createUser(@Body request: CreateUserRequest): UserDto
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* DELETE /users/{id}
|
|
94
|
+
*
|
|
95
|
+
* Success: 204 (no body)
|
|
96
|
+
* Not Found: 404 ApiErrorResponse
|
|
97
|
+
* Forbidden: 403 ApiErrorResponse
|
|
98
|
+
*/
|
|
99
|
+
@DELETE("users/{id}")
|
|
100
|
+
suspend fun deleteUser(@Path("id") id: String)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## API Versioning
|
|
107
|
+
|
|
108
|
+
```kotlin
|
|
109
|
+
// ✅ Version in base URL
|
|
110
|
+
val retrofit = Retrofit.Builder()
|
|
111
|
+
.baseUrl("https://api.example.com/v1/")
|
|
112
|
+
.build()
|
|
113
|
+
|
|
114
|
+
// ✅ Version in header (for minor versions)
|
|
115
|
+
class ApiVersionInterceptor : Interceptor {
|
|
116
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
117
|
+
val request = chain.request().newBuilder()
|
|
118
|
+
.addHeader("API-Version", "2024-01-01")
|
|
119
|
+
.build()
|
|
120
|
+
return chain.proceed(request)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ✅ Separate API interfaces per major version
|
|
125
|
+
interface UserApiV1 {
|
|
126
|
+
@GET("users/{id}")
|
|
127
|
+
suspend fun getUser(@Path("id") id: String): UserDtoV1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface UserApiV2 {
|
|
131
|
+
@GET("users/{id}")
|
|
132
|
+
suspend fun getUser(@Path("id") id: String): UserDtoV2
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Handling Additive Changes (Non-Breaking)
|
|
139
|
+
|
|
140
|
+
```kotlin
|
|
141
|
+
// ✅ New optional fields — add with default value
|
|
142
|
+
@Serializable
|
|
143
|
+
data class UserDto(
|
|
144
|
+
@SerialName("id") val id: String,
|
|
145
|
+
@SerialName("name") val name: String,
|
|
146
|
+
// New field added by backend — safe with default
|
|
147
|
+
@SerialName("phone") val phone: String? = null,
|
|
148
|
+
@SerialName("tier") val tier: String = "free"
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Handling Breaking Changes
|
|
155
|
+
|
|
156
|
+
```kotlin
|
|
157
|
+
// ✅ Field renamed — use @SerialName to maintain compatibility
|
|
158
|
+
@Serializable
|
|
159
|
+
data class UserDto(
|
|
160
|
+
// Backend renamed "username" to "full_name"
|
|
161
|
+
// Keep old mapping until all clients are updated
|
|
162
|
+
@SerialName("full_name") val name: String
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// ✅ Type changed — custom serializer as bridge
|
|
166
|
+
@Serializable
|
|
167
|
+
data class OrderDto(
|
|
168
|
+
// Backend changed status from Int to String
|
|
169
|
+
@Serializable(with = OrderStatusSerializer::class)
|
|
170
|
+
@SerialName("status") val status: OrderStatus
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Contract Testing with Fake
|
|
177
|
+
|
|
178
|
+
```kotlin
|
|
179
|
+
// ✅ Fake API implementation for tests — matches the real contract
|
|
180
|
+
class FakeUserApi : UserApi {
|
|
181
|
+
val users = mutableListOf<UserDto>()
|
|
182
|
+
|
|
183
|
+
override suspend fun getUser(id: String): UserDto {
|
|
184
|
+
return users.firstOrNull { it.id == id }
|
|
185
|
+
?: throw HttpException(
|
|
186
|
+
Response.error<UserDto>(404, "".toResponseBody())
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override suspend fun createUser(request: CreateUserRequest): UserDto {
|
|
191
|
+
val dto = UserDto(
|
|
192
|
+
id = UUID.randomUUID().toString(),
|
|
193
|
+
name = request.name,
|
|
194
|
+
email = request.email,
|
|
195
|
+
createdAt = System.currentTimeMillis()
|
|
196
|
+
)
|
|
197
|
+
users.add(dto)
|
|
198
|
+
return dto
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Anti-Patterns
|
|
206
|
+
|
|
207
|
+
- Using `Map<String, Any>` for request/response — loses type safety
|
|
208
|
+
- Not documenting expected status codes per endpoint
|
|
209
|
+
- Coupling domain models directly to API response structure
|
|
210
|
+
- Ignoring `@SerialName` — breaks when API uses snake_case
|
|
211
|
+
- No versioning strategy — every backend change becomes a breaking change
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Related Skills
|
|
216
|
+
- `serialization` — Kotlinx Serialization for DTO definition
|
|
217
|
+
- `dto-mapping` — mapping DTOs to domain models
|
|
218
|
+
- `rest` — HTTP conventions and status codes
|
|
219
|
+
- `retrofit` — implementing the API interface
|
|
220
|
+
- `error-handling` — mapping API errors to domain errors
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authentication
|
|
3
|
+
description: >
|
|
4
|
+
Authentication and token management for Android apps.
|
|
5
|
+
Load this skill when implementing login/logout flows, managing access
|
|
6
|
+
and refresh tokens, handling token expiry, securing token storage,
|
|
7
|
+
or wiring auth state to navigation.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Authentication
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Authentication in Android involves obtaining tokens (JWT or session), storing them securely, attaching them to requests, and refreshing them before expiry. The auth state drives navigation — unauthenticated users go to the login flow, authenticated users go to the main flow.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Store tokens in **EncryptedSharedPreferences** — never plain SharedPreferences or DataStore
|
|
20
|
+
- Refresh the access token **proactively** before it expires — not reactively after a 401
|
|
21
|
+
- Handle 401 responses with a **single refresh attempt** — avoid refresh loops
|
|
22
|
+
- Auth state is a `StateFlow` in a singleton — all consumers observe the same source
|
|
23
|
+
- Logout clears all tokens and navigates to the auth graph
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Token Storage
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Encrypted token storage
|
|
31
|
+
class TokenStorage @Inject constructor(
|
|
32
|
+
@ApplicationContext context: Context
|
|
33
|
+
) {
|
|
34
|
+
private val prefs = EncryptedSharedPreferences.create(
|
|
35
|
+
context,
|
|
36
|
+
"auth_prefs",
|
|
37
|
+
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
|
|
38
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
39
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
fun saveTokens(accessToken: String, refreshToken: String) {
|
|
43
|
+
prefs.edit()
|
|
44
|
+
.putString(KEY_ACCESS_TOKEN, accessToken)
|
|
45
|
+
.putString(KEY_REFRESH_TOKEN, refreshToken)
|
|
46
|
+
.apply()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun getAccessToken(): String? = prefs.getString(KEY_ACCESS_TOKEN, null)
|
|
50
|
+
fun getRefreshToken(): String? = prefs.getString(KEY_REFRESH_TOKEN, null)
|
|
51
|
+
|
|
52
|
+
fun clearTokens() {
|
|
53
|
+
prefs.edit().clear().apply()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
companion object {
|
|
57
|
+
private const val KEY_ACCESS_TOKEN = "access_token"
|
|
58
|
+
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Auth State
|
|
66
|
+
|
|
67
|
+
```kotlin
|
|
68
|
+
// ✅ Auth state as singleton StateFlow
|
|
69
|
+
sealed interface AuthState {
|
|
70
|
+
data object Loading : AuthState
|
|
71
|
+
data object Unauthenticated : AuthState
|
|
72
|
+
data class Authenticated(val userId: String) : AuthState
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Singleton
|
|
76
|
+
class AuthManager @Inject constructor(
|
|
77
|
+
private val tokenStorage: TokenStorage
|
|
78
|
+
) {
|
|
79
|
+
private val _state = MutableStateFlow<AuthState>(AuthState.Loading)
|
|
80
|
+
val state: StateFlow<AuthState> = _state.asStateFlow()
|
|
81
|
+
|
|
82
|
+
init {
|
|
83
|
+
_state.value = if (tokenStorage.getAccessToken() != null)
|
|
84
|
+
AuthState.Authenticated(getUserIdFromToken())
|
|
85
|
+
else
|
|
86
|
+
AuthState.Unauthenticated
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fun onLoginSuccess(accessToken: String, refreshToken: String, userId: String) {
|
|
90
|
+
tokenStorage.saveTokens(accessToken, refreshToken)
|
|
91
|
+
_state.value = AuthState.Authenticated(userId)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fun logout() {
|
|
95
|
+
tokenStorage.clearTokens()
|
|
96
|
+
_state.value = AuthState.Unauthenticated
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun getUserIdFromToken(): String {
|
|
100
|
+
// decode JWT or read from storage
|
|
101
|
+
return tokenStorage.getAccessToken()?.let { JwtDecoder.getUserId(it) } ?: ""
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Token Refresh Interceptor
|
|
109
|
+
|
|
110
|
+
```kotlin
|
|
111
|
+
// ✅ Single refresh attempt on 401
|
|
112
|
+
class TokenRefreshInterceptor @Inject constructor(
|
|
113
|
+
private val tokenStorage: TokenStorage,
|
|
114
|
+
private val authApi: AuthApi,
|
|
115
|
+
private val authManager: AuthManager
|
|
116
|
+
) : Interceptor {
|
|
117
|
+
|
|
118
|
+
private val refreshLock = Mutex()
|
|
119
|
+
|
|
120
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
121
|
+
val response = chain.proceed(chain.request())
|
|
122
|
+
|
|
123
|
+
if (response.code != 401) return response
|
|
124
|
+
|
|
125
|
+
response.close()
|
|
126
|
+
|
|
127
|
+
val refreshed = runBlocking {
|
|
128
|
+
refreshLock.withLock {
|
|
129
|
+
// check if another thread already refreshed
|
|
130
|
+
val currentToken = tokenStorage.getAccessToken()
|
|
131
|
+
val requestToken = chain.request().header("Authorization")
|
|
132
|
+
?.removePrefix("Bearer ")
|
|
133
|
+
|
|
134
|
+
if (currentToken != requestToken) {
|
|
135
|
+
// already refreshed by another call — use new token
|
|
136
|
+
return@withLock true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
runCatching {
|
|
140
|
+
val newTokens = authApi.refreshToken(
|
|
141
|
+
tokenStorage.getRefreshToken() ?: return@withLock false
|
|
142
|
+
)
|
|
143
|
+
tokenStorage.saveTokens(newTokens.accessToken, newTokens.refreshToken)
|
|
144
|
+
true
|
|
145
|
+
}.getOrElse {
|
|
146
|
+
authManager.logout()
|
|
147
|
+
false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return if (refreshed) {
|
|
153
|
+
val newRequest = chain.request().newBuilder()
|
|
154
|
+
.header("Authorization", "Bearer ${tokenStorage.getAccessToken()}")
|
|
155
|
+
.build()
|
|
156
|
+
chain.proceed(newRequest)
|
|
157
|
+
} else {
|
|
158
|
+
chain.proceed(chain.request())
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Auth-Gated Navigation
|
|
167
|
+
|
|
168
|
+
```kotlin
|
|
169
|
+
// ✅ Root NavHost reacts to auth state
|
|
170
|
+
@Composable
|
|
171
|
+
fun AppNavHost(authManager: AuthManager) {
|
|
172
|
+
val authState by authManager.state.collectAsStateWithLifecycle()
|
|
173
|
+
val navController = rememberNavController()
|
|
174
|
+
|
|
175
|
+
LaunchedEffect(authState) {
|
|
176
|
+
when (authState) {
|
|
177
|
+
is AuthState.Authenticated -> navController.navigate(MainGraph) {
|
|
178
|
+
popUpTo(AuthGraph) { inclusive = true }
|
|
179
|
+
}
|
|
180
|
+
is AuthState.Unauthenticated -> navController.navigate(AuthGraph) {
|
|
181
|
+
popUpTo(MainGraph) { inclusive = true }
|
|
182
|
+
}
|
|
183
|
+
is AuthState.Loading -> Unit
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
NavHost(navController = navController, startDestination = AuthGraph) {
|
|
188
|
+
navigation<AuthGraph>(startDestination = LoginRoute) { /* ... */ }
|
|
189
|
+
navigation<MainGraph>(startDestination = HomeRoute) { /* ... */ }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Anti-Patterns
|
|
197
|
+
|
|
198
|
+
- Storing tokens in plain `SharedPreferences` or `DataStore` — not encrypted
|
|
199
|
+
- Refreshing token on every request — use proactive refresh or single-retry pattern
|
|
200
|
+
- Multiple simultaneous refresh calls — use `Mutex` to serialize refresh
|
|
201
|
+
- Keeping auth state in a ViewModel — use a singleton `AuthManager`
|
|
202
|
+
- Not clearing tokens on logout — stale tokens remain accessible
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Related Skills
|
|
207
|
+
- `okhttp` — interceptor setup for auth headers
|
|
208
|
+
- `encrypted-storage` — secure storage for sensitive data
|
|
209
|
+
- `nested-navigation` — auth vs main flow navigation graphs
|
|
210
|
+
- `savedstatehandle` — preserving state across auth transitions
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: certificate-pinning
|
|
3
|
+
description: >
|
|
4
|
+
SSL/TLS certificate pinning for Android to prevent MITM attacks.
|
|
5
|
+
Load this skill when implementing certificate pinning via OkHttp,
|
|
6
|
+
configuring network security config, managing pin rotation,
|
|
7
|
+
or handling pinning failures gracefully.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Certificate Pinning
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Certificate pinning ensures the app only communicates with servers whose TLS certificate matches a known pinned value. This prevents man-in-the-middle attacks even if a rogue CA is trusted by the OS. Android supports pinning via OkHttp's `CertificatePinner` or the Network Security Config XML.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Always pin the **backup pin** alongside the primary — enables rotation without a forced update
|
|
20
|
+
- Use **public key pinning** (SPKI hash), not certificate pinning — survives certificate renewal
|
|
21
|
+
- Never pin in debug builds — breaks traffic inspection with Charles/Proxyman
|
|
22
|
+
- Have a **pin rotation plan** before shipping — expired pins = broken app
|
|
23
|
+
- Test pinning failures explicitly — they should fail closed, not open
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## OkHttp Certificate Pinner
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ CertificatePinner with primary + backup pin
|
|
31
|
+
@Provides
|
|
32
|
+
@Singleton
|
|
33
|
+
fun provideCertificatePinner(): CertificatePinner {
|
|
34
|
+
return CertificatePinner.Builder()
|
|
35
|
+
.add(
|
|
36
|
+
"api.example.com",
|
|
37
|
+
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // primary
|
|
38
|
+
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" // backup
|
|
39
|
+
)
|
|
40
|
+
.add(
|
|
41
|
+
"cdn.example.com",
|
|
42
|
+
"sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=",
|
|
43
|
+
"sha256/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD="
|
|
44
|
+
)
|
|
45
|
+
.build()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✅ Attach to OkHttpClient — skip in debug
|
|
49
|
+
@Provides
|
|
50
|
+
@Singleton
|
|
51
|
+
fun provideOkHttpClient(
|
|
52
|
+
certificatePinner: CertificatePinner,
|
|
53
|
+
@IsDebug isDebug: Boolean
|
|
54
|
+
): OkHttpClient {
|
|
55
|
+
return OkHttpClient.Builder()
|
|
56
|
+
.apply {
|
|
57
|
+
if (!isDebug) certificatePinner(certificatePinner)
|
|
58
|
+
}
|
|
59
|
+
.build()
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Network Security Config (XML)
|
|
66
|
+
|
|
67
|
+
```xml
|
|
68
|
+
<!-- res/xml/network_security_config.xml -->
|
|
69
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
70
|
+
<network-security-config>
|
|
71
|
+
<domain-config>
|
|
72
|
+
<domain includeSubdomains="true">api.example.com</domain>
|
|
73
|
+
<pin-set expiration="2026-01-01">
|
|
74
|
+
<!-- Primary pin -->
|
|
75
|
+
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
|
|
76
|
+
<!-- Backup pin -->
|
|
77
|
+
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
|
|
78
|
+
</pin-set>
|
|
79
|
+
</domain-config>
|
|
80
|
+
|
|
81
|
+
<!-- Debug: allow cleartext and trust user CAs -->
|
|
82
|
+
<debug-overrides>
|
|
83
|
+
<trust-anchors>
|
|
84
|
+
<certificates src="user"/>
|
|
85
|
+
<certificates src="system"/>
|
|
86
|
+
</trust-anchors>
|
|
87
|
+
</debug-overrides>
|
|
88
|
+
</network-security-config>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```xml
|
|
92
|
+
<!-- AndroidManifest.xml -->
|
|
93
|
+
<application
|
|
94
|
+
android:networkSecurityConfig="@xml/network_security_config"
|
|
95
|
+
...>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Extracting the Pin
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Extract SPKI hash from a live server
|
|
104
|
+
openssl s_client -connect api.example.com:443 -servername api.example.com \
|
|
105
|
+
</dev/null 2>/dev/null \
|
|
106
|
+
| openssl x509 -pubkey -noout \
|
|
107
|
+
| openssl pkey -pubin -outform DER \
|
|
108
|
+
| openssl dgst -sha256 -binary \
|
|
109
|
+
| openssl enc -base64
|
|
110
|
+
|
|
111
|
+
# Or from a certificate file
|
|
112
|
+
openssl x509 -in cert.pem -pubkey -noout \
|
|
113
|
+
| openssl pkey -pubin -outform DER \
|
|
114
|
+
| openssl dgst -sha256 -binary \
|
|
115
|
+
| openssl enc -base64
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Pinning Failure Handling
|
|
121
|
+
|
|
122
|
+
```kotlin
|
|
123
|
+
// ✅ Catch SSLPeerUnverifiedException — pinning failure
|
|
124
|
+
suspend fun safeApiCall(call: suspend () -> T): Result<T> = runCatching {
|
|
125
|
+
call()
|
|
126
|
+
}.recoverCatching { throwable ->
|
|
127
|
+
when (throwable) {
|
|
128
|
+
is SSLPeerUnverifiedException -> throw CertificatePinningException(
|
|
129
|
+
"Certificate pinning failure — possible MITM attack"
|
|
130
|
+
)
|
|
131
|
+
else -> throw throwable
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class CertificatePinningException(message: String) : SecurityException(message)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Pin Rotation Strategy
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
1. Generate new certificate/key pair
|
|
144
|
+
2. Extract SPKI hash of new cert → backup pin
|
|
145
|
+
3. Ship app update with both old (primary) + new (backup) pins
|
|
146
|
+
4. Wait for adoption (monitor crash-free rate)
|
|
147
|
+
5. Deploy new certificate to server
|
|
148
|
+
6. Ship next app update with new cert as primary + newer backup
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Anti-Patterns
|
|
154
|
+
|
|
155
|
+
- Pinning in debug builds — breaks all proxy-based debugging tools
|
|
156
|
+
- Pinning without a backup pin — one certificate renewal breaks the app
|
|
157
|
+
- Catching `SSLPeerUnverifiedException` silently — must fail closed, log the incident
|
|
158
|
+
- Using certificate pinning (full cert hash) instead of public key pinning — breaks on renewal
|
|
159
|
+
- Not setting an `expiration` in Network Security Config — no forced rotation reminder
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Related Skills
|
|
164
|
+
- `okhttp` — OkHttp client where pinner is attached
|
|
165
|
+
- `secure-networking` — broader TLS and network security
|
|
166
|
+
- `network-security-config` — XML-based network security configuration
|
|
167
|
+
- `certificate-transparency` — complementary server validation mechanism
|