From f6718417782d57d574dba34d4a29805814189904 Mon Sep 17 00:00:00 2001 From: Rich Smith Date: Wed, 11 Oct 2023 14:50:53 -0400 Subject: [PATCH] feat(api): add OIDC refresh token support to chalk (#51) * feat(api): add OIDC refresh_token support to chalk * refactor(api): identify access token variables more explicitly --- src/api.nim | 50 +++++++++++++++----- src/attestation.nim | 66 +++++++++++++++++++-------- src/configs/base_keyspecs.c4m | 13 ++++++ src/configs/base_report_templates.c4m | 1 + 4 files changed, 99 insertions(+), 31 deletions(-) diff --git a/src/api.nim b/src/api.nim index 91fcbf58..ada7e99a 100644 --- a/src/api.nim +++ b/src/api.nim @@ -20,7 +20,36 @@ template jwtSplitAndDecode(jwtString: string, doDecode: bool): string = else: $apiJwtPayload -proc getChalkApiToken*(): string = +proc refreshAccessToken*(refresh_token: string): string = + + # Mechanism to support access_token refresh via OIDC + let timeout: int = cast[int](chalkConfig.getSecretManagerTimeout()) + var + refresh_url = uri.parseUri(chalkConfig.getSecretManagerUrl()) + context: SslContext + client: HttpClient + + refresh_url.path = "/api/refresh" + + # request new access_token via refresh + info("Refreshing API access token....") + if refresh_url.scheme == "https": + let context = newContext(verifyMode = CVerifyPeer) + client = newHttpClient(sslContext = context, timeout = timeout) + else: + client = newHttpClient(timeout = timeout) + let response = client.safeRequest(url = refresh_url, httpMethod = HttpPost, body = $refresh_token) + client.close() + + if response.status.startswith("200"): + # parse json response and save / return values + let jsonNode = parseJson(response.body()) + let new_access_token = jsonNode["access_token"].getStr() + let new_id_token = jsonNode["id_token"].getStr() + + return new_access_token + +proc getChalkApiToken*(): (string, string) = # ToDo check if token already self chalked in and gecan be read @@ -43,10 +72,11 @@ proc getChalkApiToken*(): string = pollUri: Uri pollUrl: string pollInt: int + refreshToken: string response: Response responsePoll: Response - ret: string = "" - token: string + ret = ("","") + accessToken: string totalSleepTime: float = 0.0 type frameList = array[8, string] @@ -67,7 +97,6 @@ proc getChalkApiToken*(): string = # set api login endpoint var login_url = uri.parseUri(chalkConfig.getSecretManagerUrl()) - login_url.path = "/api/login" # request auth code from API @@ -82,7 +111,6 @@ proc getChalkApiToken*(): string = if response.status.startswith("200"): # parse json response and save / return values - trace(response.body()) let jsonNode = parseJson(response.body()) authId = jsonNode["id"].getStr() authUrl = jsonNode["authUrl"].getStr() @@ -119,18 +147,16 @@ proc getChalkApiToken*(): string = stdout.write(succFr) stdout.flushFile() print("
Authentication successful!
\n") - trace(responsePoll.status & responsePoll.body()) # parse json response and save / return values() let jsonPollNode = parseJson(responsePoll.body()) - token = jsonPollNode["access_token"].getStr() - trace($jsonPollNode) + accessToken = jsonPollNode["access_token"].getStr() + refreshToken = jsonPollNode["refresh_token"].getStr() # decode JWT - pollPayloadBase64 = jwtSplitAndDecode($token, true) - let decodedPollJwt = parseJson(pollPayloadBase64) - trace($decodedPollJwt) - ret = $token + pollPayloadBase64 = jwtSplitAndDecode($accessToken, true) + let decodedPollJwt = parseJson(pollPayloadBase64) + ret = ($accessToken, $refreshToken) elif responsePoll.status.startswith("428") or responsePoll.status.startswith("403"): # sleep for requested polling period while showing spinner before polling again diff --git a/src/attestation.nim b/src/attestation.nim index 7e743547..36c4f006 100644 --- a/src/attestation.nim +++ b/src/attestation.nim @@ -186,9 +186,37 @@ proc loadFromSecretManager*(prkey: string, apikey: string): bool = let response = callTheSecretService(base, prKey, apikey, "", HttpGet) if response.status[0] != '2': - warn("Could not retrieve signing secret: " & response.status & "\n" & - "Will not be able to sign / verify.") - return false + # authentication issue / token expiration - begin reauth + if response.status.startswith("401"): + # parse json response and save / return values() + let jsonNodeReason = parseJson(response.body()) + let reasonCode = jsonNodeReason["Message"].getStr() + + if reasonCode.startswith("token_expired"): + info("API access token expired, refreshing ...") + # Remove current API token from self chalk mark + selfChalk.extract["$CHALK_API_KEY"] = pack("") + + # refresh access_token + let boxedOptRefresh = selfChalkGetKey("$CHALK_API_REFRESH_TOKEN") + if boxedOptRefresh.isSome(): + let + boxedRefresh = boxedOptRefresh.get() + refreshToken = unpack[string](boxedRefresh) + trace("Refresh token retrieved from chalk mark: " & $refreshToken) + + let newApiToken = refreshAccessToken($refreshToken) + if newApiToken == "": + return false + else: + trace("API Token refreshed: " & newApiToken) + #save new api token to self chalk mark + selfChalk.extract["$CHALK_API_KEY"] = pack($newApiToken) + return loadFromSecretManager(prkey, $newApiToken) + else: + warn("Could not retrieve signing secret: " & response.status & "\n" & + "Will not be able to sign / verify.") + return false var body: string @@ -393,13 +421,14 @@ proc testSigningSetup(pubKey, priKey: string): bool = proc writeSelfConfig(selfChalk: ChalkObj): bool {.importc, discardable.} -proc saveSigningSetup(pubKey, priKey, apiToken: string, gen: bool): bool = +proc saveSigningSetup(pubKey, priKey, apiToken, refreshToken: string, gen: bool): bool = let selfChalk = getSelfExtraction().get() selfChalk.extract["$CHALK_ENCRYPTED_PRIVATE_KEY"] = pack(priKey) selfChalk.extract["$CHALK_PUBLIC_KEY"] = pack(pubKey) if apiToken != "": selfChalk.extract["$CHALK_API_KEY"] = pack(apiToken) + selfChalk.extract["$CHALK_API_REFRESH_TOKEN"] = pack(refreshToken) commitPassword(prikey, apiToken, gen) @@ -477,20 +506,10 @@ proc attemptToLoadKeys*(silent=false): bool = let withoutExtension = getKeyFileLoc() use_api = chalkConfig.getApiLogin() - var - apikey = "" if withoutExtension == "": return false - if use_api: - # get API key to pass to secret manager - let boxedOptApi = selfChalkGetKey("$CHALK_API_KEY") - if boxedOptApi.isSome(): - let boxedApi = boxedOptApi.get() - apikey = unpack[string](boxedApi) - trace("API token retrieved from chalk mark: " & $apikey) - var pubKey = tryToLoadFile(withoutExtension & ".pub") priKey = tryToLoadFile(withoutExtension & ".key") @@ -514,14 +533,23 @@ proc attemptToLoadKeys*(silent=false): bool = return false cosignLoaded = true + + # Ensure any changed chalk keys are saved to self + let savedCommandName = getCommandName() + setCommandName("setup") + result = selfChalk.writeSelfConfig() + setCommandName(savedCommandName) + return true proc attemptToGenKeys*(): bool = - var apiToken = "" - let use_api = chalkConfig.getApiLogin() + var + apiToken = "" + refreshToken = "" + let use_api = chalkConfig.getApiLogin() if use_api: - apiToken = getChalkApiToken() + (apiToken, refreshToken) = getChalkApiToken() if apiToken == "": return false else: @@ -559,9 +587,9 @@ proc attemptToGenKeys*(): bool = cosignLoaded = true if use_api: - result = saveSigningSetup(pubKey, priKey, apiToken, true) + result = saveSigningSetup(pubKey, priKey, apiToken, refreshToken, true) else: - result = saveSigningSetup(pubKey, priKey, "", true) + result = saveSigningSetup(pubKey, priKey, "", "", true) proc canAttest*(): bool = if getCosignLocation() == "": diff --git a/src/configs/base_keyspecs.c4m b/src/configs/base_keyspecs.c4m index b7240f9f..b1f5182d 100644 --- a/src/configs/base_keyspecs.c4m +++ b/src/configs/base_keyspecs.c4m @@ -4741,6 +4741,19 @@ API key used to optionally save/load attestation keys to cloud. """ } +keyspec $CHALK_API_REFRESH_TOKEN { + required_in_self_mark: true + kind: ChalkTimeArtifact + type: string + standard: true + system: true + since: "0.1.4" + doc: """ +Key to hopld the OIDC refresh token for non-user present API +re-authentication. +""" +} + keyspec $CHALK_ATTESTATION_TOKEN { required_in_self_mark: true kind: ChalkTimeArtifact diff --git a/src/configs/base_report_templates.c4m b/src/configs/base_report_templates.c4m index febafbf2..ee9c035e 100644 --- a/src/configs/base_report_templates.c4m +++ b/src/configs/base_report_templates.c4m @@ -376,6 +376,7 @@ report and subtract from it. key.$CHALK_PUBLIC_KEY.use = true key.$CHALK_ENCRYPTED_PRIVATE_KEY.use = true key.$CHALK_API_KEY.use = true + key.$CHALK_API_REFRESH_TOKEN.use = true key.$CHALK_ATTESTATION_TOKEN.use = true key.$CHALK_SECRET_ENDPOINT_URI.use = true }