Skip to content

Commit

Permalink
Merge pull request #52 from ldclakmal/dev
Browse files Browse the repository at this point in the history
Introduce API to refresh OAuth2 token and introduce default token expiry time
  • Loading branch information
ldclakmal authored Jan 15, 2021
2 parents a8dd0e3 + f17eaae commit 6df09fd
Showing 1 changed file with 81 additions and 44 deletions.
125 changes: 81 additions & 44 deletions oauth2-ballerina/client_oauth2_provider.bal
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@ import ballerina/time;
# + clientId - Client ID for the client credentials grant authentication
# + clientSecret - Client secret for the client credentials grant authentication
# + scopes - Scope(s) of the access request
# + defaultTokenExpTimeInSeconds - Expiration time of the tokens if authorization server response does not contain an `expires_in` field
# + clockSkewInSeconds - Clock skew in seconds
# + retryRequest - Retry the request if the initial request returns a 401 response
# + parameters - Map of endpoint parameters use with the authorization endpoint
# + credentialBearer - Bearer of the authentication credentials, which is sent to the authorization endpoint
# + clientConfig - HTTP client configurations, which are used to call the authorization endpoint
public type ClientCredentialsGrantConfig record {|
public type ClientCredentialsGrantConfig record {
string tokenUrl;
string clientId;
string clientSecret;
string[] scopes?;
int defaultTokenExpTimeInSeconds = 3600;
int clockSkewInSeconds = 0;
boolean retryRequest = true;
map<string> parameters?;
CredentialBearer credentialBearer = AUTH_HEADER_BEARER;
ClientConfiguration clientConfig = {};
|};
};

# The data structure, which is used to configure the OAuth2 password grant type.
#
Expand All @@ -48,40 +48,40 @@ public type ClientCredentialsGrantConfig record {|
# + clientSecret - Client secret for the password grant authentication
# + scopes - Scope(s) of the access request
# + refreshConfig - Configurations for refreshing the access token
# + defaultTokenExpTimeInSeconds - Expiration time of the tokens if authorization server response does not contain an `expires_in` field
# + clockSkewInSeconds - Clock skew in seconds
# + retryRequest - Retry the request if the initial request returns a 401 response
# + parameters - Map of endpoint parameters use with the authorization endpoint
# + credentialBearer - Bearer of the authentication credentials, which is sent to the authorization endpoint
# + clientConfig - HTTP client configurations, which are used to call the authorization endpoint
public type PasswordGrantConfig record {|
public type PasswordGrantConfig record {
string tokenUrl;
string username;
string password;
string clientId?;
string clientSecret?;
string[] scopes?;
RefreshConfig refreshConfig?;
int defaultTokenExpTimeInSeconds = 3600;
int clockSkewInSeconds = 0;
boolean retryRequest = true;
map<string> parameters?;
CredentialBearer credentialBearer = AUTH_HEADER_BEARER;
ClientConfiguration clientConfig = {};
|};
};

# The data structure, which is used to configure the OAuth2 access token directly.
#
# + accessToken - Access token for the authorization endpoint
# + refreshConfig - Configurations for refreshing the access token
# + defaultTokenExpTimeInSeconds - Expiration time of the tokens if authorization server response does not contain an `expires_in` field
# + clockSkewInSeconds - Clock skew in seconds
# + retryRequest - Retry the request if the initial request returns a 401 response
# + credentialBearer - Bearer of the authentication credentials, which is sent to the authorization endpoint
public type DirectTokenConfig record {|
public type DirectTokenConfig record {
string accessToken?;
DirectTokenRefreshConfig refreshConfig?;
int defaultTokenExpTimeInSeconds = 3600;
int clockSkewInSeconds = 0;
boolean retryRequest = true;
CredentialBearer credentialBearer = AUTH_HEADER_BEARER;
|};
};

# The data structure, which can be used to pass the configurations for refreshing the access token of
# the password grant type.
Expand Down Expand Up @@ -190,11 +190,7 @@ public class ClientOAuth2Provider {
# + grantConfig - OAuth2 grant type configurations
public isolated function init(GrantConfig grantConfig) {
self.grantConfig = grantConfig;
self.tokenCache = {
accessToken: "",
refreshToken: "",
expTime: 0
};
self.tokenCache = initTokenCache();
}

# Generate a token for the OAuth2 authentication.
Expand All @@ -210,6 +206,20 @@ public class ClientOAuth2Provider {
}
return checkpanic authToken;
}

# Refresh a token for the OAuth2 authentication.
# ```ballerina
# string:oauth2:Error token = provider.refreshToken();
# ```
#
# + return - Generated `string` token or else an `oauth2:Error` if an error occurred
public isolated function refreshToken() returns string|Error {
string|Error authToken = refreshOAuth2Token(self.grantConfig, self.tokenCache);
if (authToken is Error) {
return prepareError("Failed to refresh OAuth2 token.", authToken);
}
return checkpanic authToken;
}
}

// Generates the OAuth2 token.
Expand All @@ -223,18 +233,29 @@ isolated function generateOAuth2Token(GrantConfig grantConfig, TokenCache tokenC
}
}

// Refreshes the OAuth2 token.
isolated function refreshOAuth2Token(GrantConfig grantConfig, TokenCache tokenCache) returns string|Error {
if (grantConfig is PasswordGrantConfig) {
return getAccessTokenFromRefreshRequest(grantConfig, tokenCache);
} else if (grantConfig is ClientCredentialsGrantConfig) {
return getAccessTokenFromAuthorizationRequest(grantConfig, tokenCache);
} else {
return getAccessTokenFromRefreshRequest(grantConfig, tokenCache);
}
}

// Processes the OAuth2 token for the password grant type.
isolated function getOAuth2TokenForPasswordGrant(PasswordGrantConfig grantConfig, TokenCache tokenCache)
returns string|Error {
string cachedAccessToken = tokenCache.accessToken;
if (cachedAccessToken == "") {
return getAccessTokenFromAuthorizationRequest(grantConfig, tokenCache);
} else {
if (isCachedTokenExpired(tokenCache.expTime)) {
if (isCachedTokenValid(tokenCache.expTime)) {
return cachedAccessToken;
} else {
lock {
if (isCachedTokenExpired(tokenCache.expTime)) {
if (isCachedTokenValid(tokenCache.expTime)) {
return tokenCache.accessToken;
}
return getAccessTokenFromRefreshRequest(grantConfig, tokenCache);
Expand All @@ -250,11 +271,11 @@ isolated function getOAuth2TokenForClientCredentialsGrant(ClientCredentialsGrant
if (cachedAccessToken == "") {
return getAccessTokenFromAuthorizationRequest(grantConfig, tokenCache);
} else {
if (isCachedTokenExpired(tokenCache.expTime)) {
if (isCachedTokenValid(tokenCache.expTime)) {
return cachedAccessToken;
} else {
lock {
if (isCachedTokenExpired(tokenCache.expTime)) {
if (isCachedTokenValid(tokenCache.expTime)) {
return tokenCache.accessToken;
}
return getAccessTokenFromAuthorizationRequest(grantConfig, tokenCache);
Expand All @@ -274,11 +295,11 @@ isolated function getOAuth2TokenForDirectTokenMode(DirectTokenConfig grantConfig
}
return getAccessTokenFromRefreshRequest(grantConfig, tokenCache);
} else {
if (isCachedTokenExpired(tokenCache.expTime)) {
if (isCachedTokenValid(tokenCache.expTime)) {
return cachedAccessToken;
} else {
lock {
if (isCachedTokenExpired(tokenCache.expTime)) {
if (isCachedTokenValid(tokenCache.expTime)) {
return tokenCache.accessToken;
}
return getAccessTokenFromRefreshRequest(grantConfig, tokenCache);
Expand All @@ -287,23 +308,11 @@ isolated function getOAuth2TokenForDirectTokenMode(DirectTokenConfig grantConfig
}
}

// Checks the validity of the access token, which is in the cache. If the expiry time is 0, that means no expiry time is
// returned with the authorization request. This implies that the token is valid forever.
isolated function isCachedTokenExpired(int expTime) returns boolean {
if (expTime == 0) {
return true;
}
int currentSystemTime = time:currentTime().time;
if (currentSystemTime < expTime) {
return true;
}
return false;
}

// Requests an access token from the authorization endpoint using the provided configurations.
isolated function getAccessTokenFromAuthorizationRequest(ClientCredentialsGrantConfig|PasswordGrantConfig config,
TokenCache tokenCache) returns string|Error {
RequestConfig requestConfig;
int defaultTokenExpTimeInSeconds;
int clockSkewInSeconds;
string tokenUrl;
ClientConfiguration clientConfig;
Expand All @@ -321,6 +330,7 @@ isolated function getAccessTokenFromAuthorizationRequest(ClientCredentialsGrantC
parameters: config?.parameters,
credentialBearer: config.credentialBearer
};
defaultTokenExpTimeInSeconds = config.defaultTokenExpTimeInSeconds;
clockSkewInSeconds = config.clockSkewInSeconds;
clientConfig = config.clientConfig;
} else {
Expand All @@ -347,16 +357,18 @@ isolated function getAccessTokenFromAuthorizationRequest(ClientCredentialsGrantC
credentialBearer: config.credentialBearer
};
}
defaultTokenExpTimeInSeconds = config.defaultTokenExpTimeInSeconds;
clockSkewInSeconds = config.clockSkewInSeconds;
clientConfig = config.clientConfig;
}
return sendRequest(requestConfig, tokenUrl, clientConfig, tokenCache, clockSkewInSeconds);
return sendRequest(requestConfig, tokenUrl, clientConfig, tokenCache, defaultTokenExpTimeInSeconds, clockSkewInSeconds);
}

// Requests an access token from the authorization endpoint using the provided refresh configurations.
isolated function getAccessTokenFromRefreshRequest(PasswordGrantConfig|DirectTokenConfig config,
TokenCache tokenCache) returns string|Error {
RequestConfig requestConfig;
int defaultTokenExpTimeInSeconds;
int clockSkewInSeconds;
string refreshUrl;
ClientConfiguration clientConfig;
Expand Down Expand Up @@ -386,6 +398,7 @@ isolated function getAccessTokenFromRefreshRequest(PasswordGrantConfig|DirectTok
} else {
return prepareError("Failed to refresh access token since RefreshTokenConfig is not provided.");
}
defaultTokenExpTimeInSeconds = config.defaultTokenExpTimeInSeconds;
clockSkewInSeconds = config.clockSkewInSeconds;
} else {
DirectTokenRefreshConfig? refreshConfig = config?.refreshConfig;
Expand All @@ -406,20 +419,22 @@ isolated function getAccessTokenFromRefreshRequest(PasswordGrantConfig|DirectTok
} else {
return prepareError("Failed to refresh access token since DirectRefreshTokenConfig is not provided.");
}
defaultTokenExpTimeInSeconds = config.defaultTokenExpTimeInSeconds;
clockSkewInSeconds = config.clockSkewInSeconds;
}
return sendRequest(requestConfig, refreshUrl, clientConfig, tokenCache, clockSkewInSeconds);
return sendRequest(requestConfig, refreshUrl, clientConfig, tokenCache, defaultTokenExpTimeInSeconds, clockSkewInSeconds);
}

isolated function sendRequest(RequestConfig requestConfig, string url, ClientConfiguration clientConfig,
TokenCache tokenCache, int clockSkewInSeconds) returns string|Error {
TokenCache tokenCache, int defaultTokenExpTimeInSeconds, int clockSkewInSeconds)
returns string|Error {
map<string> headers = check prepareHeaders(requestConfig);
string payload = check preparePayload(requestConfig);
string|Error stringResponse = doHttpRequest(url, clientConfig, headers, payload);
if (stringResponse is Error) {
return prepareError("Failed to call introspection endpoint.", stringResponse);
}
return extractAccessToken(checkpanic stringResponse, tokenCache, clockSkewInSeconds);
return extractAccessToken(checkpanic stringResponse, tokenCache, defaultTokenExpTimeInSeconds, clockSkewInSeconds);
}

isolated function prepareHeaders(RequestConfig config) returns map<string>|Error {
Expand Down Expand Up @@ -472,28 +487,50 @@ isolated function preparePayload(RequestConfig config) returns string|Error {
return textPayload;
}

isolated function extractAccessToken(string response, TokenCache tokenCache, int clockSkewInSeconds)
returns string|Error {
isolated function extractAccessToken(string response, TokenCache tokenCache, int defaultTokenExpTimeInSeconds,
int clockSkewInSeconds) returns string|Error {
json|error jsonResponse = response.fromJsonString();
if (jsonResponse is error) {
return prepareError("Failed to retrieve access token since the response payload is not a JSON.", jsonResponse);
} else {
updateOAuth2CacheEntry(jsonResponse, tokenCache, clockSkewInSeconds);
updateOAuth2CacheEntry(jsonResponse, tokenCache, defaultTokenExpTimeInSeconds, clockSkewInSeconds);
return (checkpanic (jsonResponse.access_token)).toJsonString();
}
}

// Checks the validity of the cached access token.
isolated function isCachedTokenValid(int expTime) returns boolean {
int currentSystemTime = time:currentTime().time;
if (currentSystemTime < expTime) {
return true;
}
return false;
}

// Updates the OAuth2 token entry with the received JSON payload of the response.
isolated function updateOAuth2CacheEntry(json responsePayload, TokenCache tokenCache, int clockSkewInSeconds) {
isolated function updateOAuth2CacheEntry(json responsePayload, TokenCache tokenCache, int defaultTokenExpTimeInSeconds,
int clockSkewInSeconds) {
int issueTime = time:currentTime().time;
string accessToken = (checkpanic (responsePayload.access_token)).toJsonString();
tokenCache.accessToken = accessToken;
json|error expiresIn = responsePayload?.expires_in;
if (expiresIn is int) {
tokenCache.expTime = issueTime + (expiresIn - clockSkewInSeconds) * 1000;
} else {
tokenCache.expTime = issueTime + (defaultTokenExpTimeInSeconds - clockSkewInSeconds) * 1000;
}
if (responsePayload.refresh_token is string) {
string refreshToken = (checkpanic (responsePayload.refresh_token)).toJsonString();
tokenCache.refreshToken = refreshToken;
}
}

// Initialize OAuth2 token entry with the default exp time of provided configurations.
isolated function initTokenCache() returns TokenCache {
TokenCache tokenCache = {
accessToken: "",
refreshToken: "",
expTime: -1
};
return tokenCache;
}

0 comments on commit 6df09fd

Please # to comment.