-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/11 client library for convenience (#81)
* Create Initial Library * Basic Example using the Lib * Added features to example * Added Documentation to Library * Minor Refactoring * Initial Changes to separate out example and add library to nexus * Add License and let Project Build * Add Lib from Nexus to Application.scala * Changed from external to internal dependency * Remove Credentials * Split Public Key from Token retrieval * Rename Retrieval Service Classes * Initial SBT Refactoring * Remove Decoder Class in order to use NimbusDecoder Directly * Minor Refactoring from comments * Added DecoderProvider * Add Tests for ClaimsParser * Fix Tests * Fix Tests decoder version * Minor Refactoring * Minor ReadMe Refactoring * #11 change suggestions (#84) * Refactoring and Verificator Tests * Readme Additions * Added New Test Cases * #11 change suggestions 2 (#85) * Addition to Developer list --------- Co-authored-by: Daniel K <[email protected]>
- Loading branch information
1 parent
58b7f19
commit b9c9230
Showing
23 changed files
with
1,448 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# Login-service client library | ||
|
||
This library provides client-functionality for the login-service. | ||
|
||
## Usage | ||
|
||
Include the library in your project: | ||
### Maven | ||
|
||
`<dependency> | ||
<groupId>za.co.absa</groupId> | ||
<artifactId>login-service-client-library_2.12</artifactId> | ||
<version>$VERSION</version> | ||
</dependency>` | ||
|
||
### SBT | ||
|
||
`libraryDependencies += "za.co.absa" % "login-service-client-library_2.12" % "0.1.0-SNAPSHOT"` | ||
|
||
See the [examples](examples) | ||
for a more detailed view of how to use the library. | ||
|
||
## Public key retrieval | ||
|
||
The library provides a `PublicKeyRetrievalClient` class that can be used to retrieve the public key to verify tokens' signatures. | ||
Public Key is available without authorization so just the relevant host needs to be provided. Public Key is available as a `String` and as a JWKS. | ||
|
||
## Token retrieval | ||
|
||
The library provides a `TokenRetrievalClient` class that can be used to retrieve access and refresh tokens. | ||
Refresh and Access Keys require authorization. Basic Auth is used for the initial retrieval so a valid username and password is required. | ||
Please see the [login-service documentation](README.md) for more information on what a valid username and password is. | ||
Refresh token from initial retrieval is used to refresh the access token. | ||
|
||
## Creating and Using a JWT Decoder | ||
|
||
The User can create and use the `org.springframework.security.oauth2.jwt.NimbusJwtDecoder` by utilizing the 'JwtDecoderProvider' object. | ||
This allows the user to create the decoder from a publicKey object, String or URL. | ||
|
||
## Parsing and using Claims | ||
|
||
`AccessTokenClaimsParser` object is used to parse decoded Access Token claims. | ||
`RefreshTokenClaimsParser` object is used to parse decoded Refresh Token claims. | ||
Both are used to extract the claims from the respective decoded jwt which can be used to check and verify the token claims. | ||
|
||
For example, one may check an access token for the `groups` claim to indicate what a user may or may not do. | ||
|
||
## Token Verification | ||
|
||
The TokenVerifiers are used to verify if a token is valid. | ||
The `AccessTokenVerifier` is used to verify an access token. | ||
The `RefreshTokenVerifier` is used to verify a refresh token. | ||
|
||
These verifiers check if the token has the following: | ||
1. A valid signature | ||
2. The token is not expired | ||
3. The token is of the correct type | ||
|
||
It will Return a JWT Object with claims that can be read if the token is valid. | ||
|
||
## Example Code | ||
|
||
An example of how to use the library can be found in the [examples](examples) folder. | ||
The example makes use of a [configuration file](examples/src/main/resources/example.application.yaml) to provide the necessary configuration to the library. | ||
|
||
Configurations required are: | ||
1. `host` - the url of the login-service (Including Port if required) | ||
2. 'refresh-period' - the period between refreshing the public-key used for verification. This Parameter is optional. |
59 changes: 59 additions & 0 deletions
59
...tLibrary/src/main/scala/za/co/absa/loginclient/authorization/AccessTokenVerificator.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* Copyright 2023 ABSA Group Limited | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package za.co.absa.loginclient.authorization | ||
|
||
import org.springframework.security.oauth2.jwt.{Jwt, JwtDecoder, NimbusJwtDecoder} | ||
import za.co.absa.loginclient.exceptions.LsJwtException | ||
import za.co.absa.loginclient.tokenRetrieval.model.AccessToken | ||
|
||
import java.time.Instant | ||
|
||
case class AccessTokenVerificator( | ||
decoder: JwtDecoder | ||
) { | ||
|
||
def decodeAndVerifyAccessToken(accessToken: AccessToken): Jwt = { | ||
|
||
val jwt = try decoder.decode(accessToken.token) | ||
catch { | ||
case e: Throwable => throw LsJwtException(s"Access Token Decoding Failed: ${e.getMessage}", e) | ||
} | ||
|
||
val verificationSuccess = verifyDecodedAccessToken(jwt) | ||
|
||
if (!verificationSuccess){ | ||
throw LsJwtException("Access Token Verification Failed") | ||
} | ||
|
||
jwt | ||
} | ||
|
||
/** | ||
* Verifies that the JWT is a valid access token. | ||
* Checks that the token is not expired and that the type is access. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return True if the JWT is a valid access token, false otherwise. | ||
*/ | ||
private[authorization] def verifyDecodedAccessToken(jwt: Jwt): Boolean = { | ||
val exp = AccessTokenClaimsParser.getExpiration(jwt) | ||
val notExpired = exp.isAfter(Instant.now()) | ||
|
||
val isAccessType = AccessTokenClaimsParser.isAccessTokenType(jwt) | ||
notExpired && isAccessType | ||
} | ||
} |
185 changes: 185 additions & 0 deletions
185
clientLibrary/src/main/scala/za/co/absa/loginclient/authorization/ClaimsParser.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/* | ||
* Copyright 2023 ABSA Group Limited | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package za.co.absa.loginclient.authorization | ||
|
||
import org.springframework.security.oauth2.jwt.Jwt | ||
import za.co.absa.loginclient.exceptions.LsJwtException | ||
|
||
import java.time.Instant | ||
import java.util | ||
import scala.collection.JavaConverters._ | ||
|
||
trait ClaimsParser { | ||
|
||
/** | ||
* Returns a list of all the claim keys in the JWT. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return A list of all the claim keys in the JWT. | ||
*/ | ||
def listClaimKeys(jwt: Jwt): List[String] = { | ||
jwt.getClaims.keySet().asScala.toList | ||
} | ||
|
||
/** | ||
* Returns the value of the claim with the given key. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @param claimKey The key of the claim to retrieve. | ||
* @return The value of the claim with the given key. | ||
*/ | ||
def getClaim(jwt: Jwt, claimKey: String): Option[Any] = { | ||
Option(jwt.getClaim(claimKey)) | ||
} | ||
|
||
/** | ||
* Returns a map of all the claims in the JWT. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return A map of all the claims in the JWT. | ||
*/ | ||
def getAllClaims(jwt: Jwt): Map[String, Any] = { | ||
jwt.getClaims.asScala.toMap | ||
} | ||
|
||
/** | ||
* Returns the username of the user that the JWT was issued to. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return The username of the user that the JWT was issued to. | ||
*/ | ||
def getSubject(jwt: Jwt): String = { | ||
getClaim(jwt, "sub") match { | ||
case Some(username) => username.toString | ||
case None => throw LsJwtException("Subject not found") | ||
} | ||
} | ||
|
||
/** | ||
* Returns the expiry time of the JWT. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return The expiry time of the JWT. | ||
*/ | ||
def getExpiration(jwt: Jwt): Instant = { | ||
getClaim(jwt, "exp") match { | ||
case Some(expiration) => Instant.parse(expiration.toString) | ||
case None => throw LsJwtException("Expiration not found") | ||
} | ||
} | ||
|
||
/** | ||
* Returns the issue time of the JWT. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return The issue time of the JWT. | ||
*/ | ||
def getIssueTime(jwt: Jwt): Instant = { | ||
getClaim(jwt, "iat") match { | ||
case Some(issueTime) => Instant.parse(issueTime.toString) | ||
case None => throw LsJwtException("Issue time not found") | ||
} | ||
} | ||
|
||
/** | ||
* Returns the type of JWT. Can be either an access or refresh token. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return The type of JWT as a String. | ||
*/ | ||
def getTokenType(jwt: Jwt): String = { | ||
getClaim(jwt, "type") match { | ||
case Some(tokenType) => tokenType.toString | ||
case None => throw LsJwtException("Token type not found") | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* This object is used to parse Access Token claims. | ||
*/ | ||
|
||
object AccessTokenClaimsParser extends ClaimsParser { | ||
|
||
/** | ||
* Returns the list of groups of the user that the JWT was issued to. | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return The List of groups of the user that the JWT was issued to. | ||
*/ | ||
def getGroups(jwt: Jwt): List[String] = { | ||
getClaim(jwt, "groups") match { | ||
case Some(groups) => groups.asInstanceOf[util.ArrayList[String]].asScala.toList | ||
case None => List() | ||
} | ||
} | ||
|
||
/** | ||
* Returns the email of the user that the JWT was issued to. | ||
* @param jwt The JWT to parse. | ||
* @return The email of the user that the JWT was issued to. | ||
*/ | ||
def getEmail(jwt: Jwt): Option[String] = { | ||
getClaim(jwt, "email") match { | ||
case Some(email) => Some(email.toString) | ||
case None => None | ||
} | ||
} | ||
|
||
/** | ||
* Returns the display name of the user that the JWT was issued to. | ||
* @param jwt The JWT to parse. | ||
* @return The display name of the user that the JWT was issued to. | ||
*/ | ||
def getDisplayName(jwt: Jwt): Option[String] = { | ||
getClaim(jwt, "displayname") match { | ||
case Some(displayName) => Some(displayName.toString) | ||
case None => None | ||
} | ||
} | ||
|
||
/** | ||
* Checks the type of JWT to be 'access' | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return true if type=access was found, false otherwise | ||
* @throws LsJwtException if `type` claim was not found at all | ||
*/ | ||
|
||
def isAccessTokenType(jwt: Jwt): Boolean = { | ||
getTokenType(jwt) == "access" | ||
} | ||
} | ||
|
||
/** | ||
* This object is used to parse Refresh Token claims. | ||
*/ | ||
object RefreshTokenClaimsParser extends ClaimsParser { | ||
|
||
/** | ||
* Checks the type of JWT to be 'refresh' | ||
* | ||
* @param jwt The JWT to parse. | ||
* @return true if type=refresh was found, false otherwise | ||
* @throws LsJwtException if `type` claim was not found at all | ||
*/ | ||
|
||
def isRefreshTokenType(jwt: Jwt): Boolean = { | ||
getTokenType(jwt) == "refresh" | ||
} | ||
} |
Oops, something went wrong.