Skip to content

Commit

Permalink
feat(api): add OIDC refresh token support to chalk (#51)
Browse files Browse the repository at this point in the history
* feat(api): add OIDC refresh_token support to chalk

* refactor(api): identify access token variables more explicitly
  • Loading branch information
MyNameIsMeerkat authored Oct 11, 2023
1 parent 2a1e45f commit f671841
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 31 deletions.
50 changes: 38 additions & 12 deletions src/api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -119,18 +147,16 @@ proc getChalkApiToken*(): string =
stdout.write(succFr)
stdout.flushFile()
print("<h5>Authentication successful!</h5>\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
Expand Down
66 changes: 47 additions & 19 deletions src/attestation.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down Expand Up @@ -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() == "":
Expand Down
13 changes: 13 additions & 0 deletions src/configs/base_keyspecs.c4m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/configs/base_report_templates.c4m
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit f671841

Please sign in to comment.