@@ -23,16 +23,14 @@ import kotlinx.coroutines.Dispatchers
2323import kotlinx.coroutines.asExecutor
2424import kotlinx.coroutines.future.await
2525import org.openziti.edge.ApiClient
26+ import org.openziti.edge.ApiException
2627import org.openziti.edge.api.AuthenticationApi
2728import org.openziti.edge.api.CurrentApiSessionApi
28- import org.openziti.edge.model.Authenticate
29- import org.openziti.edge.model.EnvInfo
30- import org.openziti.edge.model.SdkInfo
29+ import org.openziti.edge.model.CurrentApiSessionDetail
3130import org.openziti.impl.ZitiImpl
3231import org.openziti.util.Logged
33- import org.openziti.util.SystemInfoProvider
34- import org.openziti.util.Version
3532import org.openziti.util.ZitiLog
33+ import java.net.HttpURLConnection.HTTP_UNAUTHORIZED
3634import java.net.URI
3735import java.net.URLEncoder
3836import java.net.http.HttpClient
@@ -41,8 +39,7 @@ import java.net.http.HttpResponse
4139import java.nio.charset.StandardCharsets
4240import java.security.MessageDigest
4341import java.time.OffsetDateTime
44- import java.util.Base64
45- import java.util.function.Consumer
42+ import java.util.*
4643import javax.net.ssl.SSLContext
4744import kotlin.random.Random
4845
@@ -52,6 +49,9 @@ interface ZitiAuthenticator {
5249 BEARER , API_SESSION
5350 }
5451
52+ class AuthException (cause : Throwable ? = null , msg : String = " not authorized" )
53+ : RuntimeException (cause?.message ? : msg, cause)
54+
5555 data class ZitiAccessToken (
5656 val type : TokenType ,
5757 val token : String ,
@@ -67,25 +67,9 @@ internal fun authenticator(ep: String, ssl: SSLContext, oidc: Boolean): ZitiAuth
6767 else
6868 LegacyAuth (ep, ssl)
6969
70- class LegacyAuth (val ep : String , val ssl : SSLContext ) : ZitiAuthenticator, Logged by ZitiLog() {
71-
72- private val auth = Authenticate ().apply {
73- val info = SystemInfoProvider ().getSystemInfo()
74- sdkInfo = SdkInfo ()
75- .type(" ziti-sdk-java" )
76- .version(Version .version)
77- .branch(Version .branch)
78- .revision(Version .revision)
79- .appId(ZitiImpl .appId)
80- .appVersion(ZitiImpl .appVersion)
81- envInfo = EnvInfo ()
82- .arch(info.arch)
83- .os(info.os)
84- .osRelease(info.osRelease)
85- .osVersion(info.osVersion)
86- configTypes = listOf (" all" )
87- }
70+ class LegacyAuth (val ep : String , val ssl : SSLContext ) : ZitiAuthenticator, Logged by ZitiLog(" legacyAuth[$ep ]" ) {
8871
72+ lateinit var apiSession: CurrentApiSessionDetail
8973 private val http = HttpClient .newBuilder()
9074 .sslContext(ssl)
9175 .executor(Dispatchers .IO .asExecutor())
@@ -95,31 +79,43 @@ class LegacyAuth(val ep: String, val ssl: SSLContext) : ZitiAuthenticator, Logge
9579 updateBaseUri(ep)
9680 }
9781
98- override suspend fun login (): ZitiAuthenticator .ZitiAccessToken {
82+ override suspend fun login (): ZitiAuthenticator .ZitiAccessToken = runCatching {
83+ d { " starting authentication" }
9984 val authApi = AuthenticationApi (api)
100- val session = authApi.authenticate(" cert" , auth).await()
101- api.requestInterceptor = Consumer {
102- req -> req.header(" zt-session" , session.data.token)
103- }
104- return ZitiAuthenticator .ZitiAccessToken (
85+ apiSession = authApi.authenticate(" cert" , ZitiImpl .loginInfo).await().data
86+ d { " authenticated successfully session.expiresAt[${apiSession.expiresAt} ]" }
87+ ZitiAuthenticator .ZitiAccessToken (
10588 ZitiAuthenticator .TokenType .API_SESSION ,
106- session.data .token,
107- session.data .expiresAt
89+ apiSession .token,
90+ apiSession .expiresAt
10891 )
92+ }.getOrElse { ex ->
93+ e(ex) { " failed to authenticate with error: ${ex.message} " }
94+ if (ex is ApiException && ex.code == HTTP_UNAUTHORIZED ) {
95+ throw ZitiAuthenticator .AuthException (ex)
96+ }
97+ throw ex
10998 }
11099
111- override suspend fun refresh (): ZitiAuthenticator .ZitiAccessToken {
100+ override suspend fun refresh (): ZitiAuthenticator .ZitiAccessToken = runCatching {
101+ d { " refreshing API session" }
112102 val currentApiSessionApi = CurrentApiSessionApi (api)
113- val session = currentApiSessionApi.currentAPISession. await()
114- return ZitiAuthenticator .ZitiAccessToken (
103+ apiSession = currentApiSessionApi.getCurrentAPISession( mapOf ( " zt-session " to apiSession.token)). await().data
104+ ZitiAuthenticator .ZitiAccessToken (
115105 ZitiAuthenticator .TokenType .API_SESSION ,
116- session.data .token,
117- session.data .expiresAt
106+ apiSession .token,
107+ apiSession .expiresAt
118108 )
109+ }.getOrElse { ex ->
110+ e(ex) { " failed to authenticate with error: ${ex.message} " }
111+ if (ex is ApiException && ex.code == HTTP_UNAUTHORIZED ) {
112+ throw ZitiAuthenticator .AuthException (ex)
113+ }
114+ throw ex
119115 }
120116}
121117
122- class InternalOIDC (val ep : String , ssl : SSLContext ): ZitiAuthenticator, Logged by ZitiLog() {
118+ class InternalOIDC (val ep : String , ssl : SSLContext ): ZitiAuthenticator, Logged by ZitiLog(" oidc[ $ep ] " ) {
123119
124120 companion object {
125121 const val CLIENT_ID = " openziti"
@@ -128,6 +124,7 @@ class InternalOIDC(val ep: String, ssl: SSLContext): ZitiAuthenticator, Logged b
128124 const val DISCOVERY = " /oidc/.well-known/openid-configuration"
129125 const val TOKEN_EXCHANGE_GRANT = " urn:ietf:params:oauth:grant-type:token-exchange"
130126 val json: ObjectMapper = ObjectMapper ().registerModule(kotlinModule())
127+ val CLIENT_ID_BASIC_AUTH = " Basic ${Encoder .encodeToString(" $CLIENT_ID :" .toByteArray())} "
131128 }
132129
133130
@@ -147,6 +144,7 @@ class InternalOIDC(val ep: String, ssl: SSLContext): ZitiAuthenticator, Logged b
147144 }
148145
149146 private suspend fun startAuth (authEndpoint : String , challenge : String , state : String ): URI {
147+ d { " starting auth" }
150148 val form = mapOf (
151149 " response_type" to " code" ,
152150 " client_id" to CLIENT_ID ,
@@ -219,6 +217,7 @@ class InternalOIDC(val ep: String, ssl: SSLContext): ZitiAuthenticator, Logged b
219217 .POST (HttpRequest .BodyPublishers .ofString(body)).build()
220218
221219 val tokenResp = http.sendAsync(req, HttpResponse .BodyHandlers .ofString()).await()
220+ tokenResp.sslSession().get().peerCertificates[0 ].publicKey
222221 return json.readTree(tokenResp.body())
223222 }
224223
@@ -242,36 +241,40 @@ class InternalOIDC(val ep: String, ssl: SSLContext): ZitiAuthenticator, Logged b
242241 require(st == state){ " OIDC state mismatch" }
243242
244243 tokens = getTokens(URI .create(tokenEndpoint), code, codeVerifier)
245- d{ " OIDC tokens: $tokens " }
246244
247245 val accessToken = tokens[" access_token" ]?.textValue()
248246 ? : throw Exception (" Missing access token in OIDC response" )
247+
249248 val exp = OffsetDateTime .now().plusSeconds(tokens[" expires_in" ]?.longValue() ? : 600 )
250249 return ZitiAuthenticator .ZitiAccessToken (ZitiAuthenticator .TokenType .BEARER , accessToken, exp)
251250 }
252251
253252 override suspend fun refresh (): ZitiAuthenticator .ZitiAccessToken {
253+ d { " starting refresh" }
254254 val refreshToken = tokens.get(" refresh_token" )?.textValue()
255255
256- if (refreshToken == null ) return login( )
256+ refreshToken ? : throw ZitiAuthenticator . AuthException (msg = " no refresh_token " )
257257
258258 val form = mapOf (
259- " grant_type" to TOKEN_EXCHANGE_GRANT ,
260- " requested_token_type" to " urn:ietf:params:oauth:token-type:refresh_token" ,
261- " subject_token_type" to " urn:ietf:params:oauth:token-type:refresh_token" ,
262- " subject_token" to refreshToken,
259+ " client_id" to CLIENT_ID ,
260+ " grant_type" to " refresh_token" ,
261+ " refresh_token" to refreshToken,
263262 )
264263
265264 val req = HttpRequest .newBuilder()
266265 .uri(config[" token_endpoint" ]?.textValue()?.let { URI .create(it) })
267- .header(" Accept" , " application/x-www-form-urlencoded" )
266+ .header(" Content-Type" , " application/x-www-form-urlencoded" )
267+ .header(" Authorization" , CLIENT_ID_BASIC_AUTH )
268268 .POST (HttpRequest .BodyPublishers .ofString(formatForm(form)))
269269 .build()
270270
271271 val resp = http.sendAsync(req, HttpResponse .BodyHandlers .ofString()).await()
272272
273+ if (resp.statusCode() == HTTP_UNAUTHORIZED ) {
274+ throw ZitiAuthenticator .AuthException ()
275+ }
273276 if (resp.statusCode() != 200 ) {
274- return login( )
277+ throw Exception ( " unexpected refresh response $resp " )
275278 }
276279
277280 tokens = json.readTree(resp.body())
0 commit comments