-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathOAuth2Client.kt
More file actions
169 lines (143 loc) · 6.13 KB
/
OAuth2Client.kt
File metadata and controls
169 lines (143 loc) · 6.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package com.coder.toolbox.oauth
import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.sdk.CoderHttpClientBuilder
import com.coder.toolbox.sdk.convertors.LoggingConverterFactory
import com.coder.toolbox.sdk.ex.ClientRegistrationException
import com.coder.toolbox.sdk.ex.OAuthTokenResponseException
import com.coder.toolbox.views.state.CoderOAuthSessionContext
import com.squareup.moshi.Moshi
import okhttp3.Credentials
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
private const val DISCOVERY_PATH = ".well-known/oauth-authorization-server"
class OAuth2Client(private val context: CoderToolboxContext) {
private val service = createAuthorizationService()
suspend fun discoverMetadata(baseUrl: String): AuthorizationServer? {
val response = service.discoverMetadata("$baseUrl/$DISCOVERY_PATH")
if (response.isSuccessful) {
return response.body()
}
context.logger.info("OAuth discovery failed: ${response.code()} ${response.message()} || ${response.errorBody()}")
return null
}
suspend fun registerClient(url: String, request: ClientRegistrationRequest): ClientRegistrationResponse {
// TODO - until https://github.com/coder/coder/issues/20370 is delivered
val response = service.registerClient(url, request)
if (response.isSuccessful) {
return requireNotNull(response.body()) { "Successful response returned null body or client registration metadata" }
}
val errorBody = response.errorBody()?.string()
val registrationError =
errorBody?.let { ClientRegistrationErrorResponse.fromJson(it) }?.toMessage() ?: response.message()
val errorMessage = "OAuth2 client registration failed with status ${response.code()}: $registrationError"
throw ClientRegistrationException(errorMessage)
}
suspend fun exchangeCode(
oauthSessionContext: CoderOAuthSessionContext,
code: String
): OAuthTokenResponse {
val auth = when (oauthSessionContext.tokenAuthMethod) {
TokenEndpointAuthMethod.CLIENT_SECRET_BASIC -> ClientAuth.ClientSecretBasic(
oauthSessionContext.clientId,
oauthSessionContext.clientSecret
)
TokenEndpointAuthMethod.CLIENT_SECRET_POST -> ClientAuth.ClientSecretPost(
oauthSessionContext.clientId,
oauthSessionContext.clientSecret
)
else -> ClientAuth.None(oauthSessionContext.clientId)
}
val response = service.exchangeCode(
url = oauthSessionContext.tokenEndpoint,
headers = auth.headers(),
fields = auth.fields() + mapOf(
"code" to code,
"grant_type" to "authorization_code",
"code_verifier" to oauthSessionContext.tokenCodeVerifier,
"redirect_uri" to "jetbrains://gateway/com.coder.toolbox/auth"
)
)
return handleResponse(response, "exchange code for token")
}
suspend fun refreshToken(oauthSessionContext: CoderOAuthSessionContext): OAuthTokenResponse {
val refreshToken = oauthSessionContext.tokenResponse?.refreshToken
?: throw IllegalStateException("No refresh token available")
val auth = when (oauthSessionContext.tokenAuthMethod) {
TokenEndpointAuthMethod.CLIENT_SECRET_BASIC -> ClientAuth.ClientSecretBasic(
oauthSessionContext.clientId,
oauthSessionContext.clientSecret
)
TokenEndpointAuthMethod.CLIENT_SECRET_POST -> ClientAuth.ClientSecretPost(
oauthSessionContext.clientId,
oauthSessionContext.clientSecret
)
else -> ClientAuth.None(oauthSessionContext.clientId)
}
val response = service.refreshToken(
url = oauthSessionContext.tokenEndpoint,
headers = auth.headers(),
fields = auth.fields() + mapOf(
"grant_type" to "refresh_token",
"refresh_token" to refreshToken
)
)
return handleResponse(response, "refresh OAuth token")
}
private fun handleResponse(
response: Response<OAuthTokenResponse>,
action: String
): OAuthTokenResponse {
if (response.isSuccessful) {
return response.body() ?: throw Exception("Failed to $action. Response body is empty.")
}
val errorBody = response.errorBody()?.string()
val tokenError = errorBody?.let { OAuthTokenErrorResponse.fromJson(it) }?.toMessage() ?: response.message()
val errorMessage = "Failed to $action. Response code: ${response.code()} $tokenError"
throw OAuthTokenResponseException(errorMessage)
}
private fun createAuthorizationService(): CoderAuthorizationApi {
return Retrofit.Builder()
.baseUrl("http://localhost/") // Placeholder, overridden by @Url
.client(CoderHttpClientBuilder.default(context))
.addConverterFactory(
LoggingConverterFactory.wrap(
context,
MoshiConverterFactory.create(Moshi.Builder().build())
)
)
.build()
.create(CoderAuthorizationApi::class.java)
}
}
private sealed interface ClientAuth {
fun headers(): Map<String, String> = emptyMap()
fun fields(): Map<String, String> = emptyMap()
data class ClientSecretBasic(
val clientId: String,
val clientSecret: String
) : ClientAuth {
override fun headers() = mapOf(
"Authorization" to Credentials.basic(
clientId,
clientSecret
)
)
}
data class ClientSecretPost(
val clientId: String,
val clientSecret: String
) : ClientAuth {
override fun fields() = mapOf(
"client_id" to clientId,
"client_secret" to clientSecret
)
}
data class None(
val clientId: String
) : ClientAuth {
override fun fields() = mapOf(
"client_id" to clientId
)
}
}