Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation options into OktaJWT library #56

Merged
merged 1 commit into from
Jun 30, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 53 additions & 31 deletions Sources/OktaJWT.swift
Original file line number Diff line number Diff line change
@@ -12,10 +12,28 @@

import Foundation

let VERSION = "2.0.1"
let VERSION = "2.1.0"

public struct OktaJWTValidator {

public struct ValidationOptions: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}

public static let issuer = ValidationOptions(rawValue: 1 << 0)
public static let audience = ValidationOptions(rawValue: 1 << 1)
public static let expiration = ValidationOptions(rawValue: 1 << 2)
public static let issuedAt = ValidationOptions(rawValue: 1 << 3)
public static let nonce = ValidationOptions(rawValue: 1 << 4)
public static let signature = ValidationOptions(rawValue: 1 << 5)

public static let allOptions: ValidationOptions = [.issuer, .audience, .expiration, .issuedAt, nonce, .signature]
}

public var keyStorageManager: PublicKeyStorageProtocol?
public var validationOptionsSet = ValidationOptions.allOptions
private var validatorOptions: [String: Any]
private var validationType: OktaJWTVerificationType
private var jwk: [String: String]?
@@ -104,58 +122,36 @@ public struct OktaJWTValidator {
}

// Check for valid issuer
if !OktaJWTVerifier.hasValidIssuer(jwt.payload.issuer, validIssuer: self.validatorOptions["iss"] as? String) {
if validationOptionsSet.contains(.issuer) && !OktaJWTVerifier.hasValidIssuer(jwt.payload.issuer, validIssuer: self.validatorOptions["iss"] as? String) {
throw OktaJWTVerificationError.invalidIssuer
}

// Check for valid audience
if !OktaJWTVerifier.hasValidAudience(jwt.payload.audience, validAudience: self.validatorOptions["aud"] as? String) {
if validationOptionsSet.contains(.audience) && !OktaJWTVerifier.hasValidAudience(jwt.payload.audience, validAudience: self.validatorOptions["aud"] as? String) {
throw OktaJWTVerificationError.invalidAudience
}

// TODO Support azp claim

// Validate the JWK signature
var key: RSAKey
switch self.validationType {
case .JWK:
guard let kid = Utils.getKeyIdFromHeader(jwt.decodedDataForPart(.header)) else {
throw OktaJWTVerificationError.noKIDFromJWT
}

if let givenJWK = self.jwk, let givenJWKId = givenJWK["kid"] {
if givenJWKId != kid {
throw OktaJWTVerificationError.invalidKID
}
key = try self.getOrRetrieveKey(jwk: givenJWK, kid:givenJWKId)
break
}
key = try self.getOrRetrieveKey(jwk: nil, kid: kid)
case .RSAKey:
key = self.key!
}

let signatureValidation = RSAPKCS1VerifierFactory.createVerifier(key: key, hashFunction: hashFunction).validateToken(jwt)
if !signatureValidation.isValid {
throw OktaJWTVerificationError.invalidSignature
}

// Validate the exp claim with or without leeway
if let validateExp = self.validatorOptions["exp"] as? Bool, validateExp == true {
if validationOptionsSet.contains(.expiration),
let validateExp = self.validatorOptions["exp"] as? Bool, validateExp == true {
if OktaJWTVerifier.isExpired(jwt.payload.expiration, leeway: self.validatorOptions["leeway"] as? Int) {
throw OktaJWTVerificationError.expiredJWT
}
}

// Validate the iat claim with or without leeway
if let validateIssuedAt = self.validatorOptions["iat"] as? Bool, validateIssuedAt == true {
if validationOptionsSet.contains(.issuedAt),
let validateIssuedAt = self.validatorOptions["iat"] as? Bool, validateIssuedAt == true {
if OktaJWTVerifier.isIssuedInFuture(jwt.payload.issuedAt, leeway: self.validatorOptions["leeway"] as? Int) {
throw OktaJWTVerificationError.issuedInFuture
}
}

// Validate the nonce claim
if let nonce = jwt.payload.jsonPayload["nonce"] as? String {
if validationOptionsSet.contains(.nonce),
let nonce = jwt.payload.jsonPayload["nonce"] as? String {
if !OktaJWTVerifier.hasValidNonce(nonce, validNonce: self.validatorOptions["nonce"] as? String) {
throw OktaJWTVerificationError.invalidNonce
}
@@ -170,7 +166,33 @@ public struct OktaJWTValidator {
if !OktaJWTVerifier.hasValue(jwt.payload.jsonPayload[k] as? String, validClaim: v as? String) {
throw OktaJWTVerificationError.invalidClaim(v)
}
}

if validationOptionsSet.contains(.signature) {
// Validate the JWK signature
var key: RSAKey
switch self.validationType {
case .JWK:
guard let kid = Utils.getKeyIdFromHeader(jwt.decodedDataForPart(.header)) else {
throw OktaJWTVerificationError.noKIDFromJWT
}

if let givenJWK = self.jwk, let givenJWKId = givenJWK["kid"] {
if givenJWKId != kid {
throw OktaJWTVerificationError.invalidKID
}
key = try self.getOrRetrieveKey(jwk: givenJWK, kid:givenJWKId)
break
}
key = try self.getOrRetrieveKey(jwk: nil, kid: kid)
case .RSAKey:
key = self.key!
}

let signatureValidation = RSAPKCS1VerifierFactory.createVerifier(key: key, hashFunction: hashFunction).validateToken(jwt)
if !signatureValidation.isValid {
throw OktaJWTVerificationError.invalidSignature
}
}

return true
8 changes: 6 additions & 2 deletions Sources/RequestsAPI.swift
Original file line number Diff line number Diff line change
@@ -17,8 +17,12 @@ open class RequestsAPI: NSObject {
/// Internal. Only for tests reason.
private static var urlSession = URLSession.shared

/// Internal. Only for tests reason.
static func setURLSession(_ urlSession: URLSession) {
/**
Overrides default URLSession object
- parameters:
- urlSession: Custom URLSession object
*/
public class func setURLSession(_ urlSession: URLSession) {
self.urlSession = urlSession
}

73 changes: 73 additions & 0 deletions Tests/iOS/JWTTests.swift
Original file line number Diff line number Diff line change
@@ -256,6 +256,79 @@ class JWTTests: XCTestCase {
XCTAssertEqual(desc.localizedDescription, "String injected is not formatted as a JSON Web Token")
}
}

func testValidationClaimsSet() {
// Issuer claim
var options: [String: Any] = [ "issuer": "https://myrealsite.com" ]
var validator = OktaJWTValidator(options)
var validationOptionsSet = OktaJWTValidator.ValidationOptions.allOptions
validationOptionsSet.remove(.signature)
XCTAssertThrowsError(try validator.isValid(jwts["OktaJWT"] as! String)) { error in
XCTAssertTrue(error is OktaJWTVerificationError)
XCTAssertEqual(error.localizedDescription, "Token issuer does not match the valid issuer")
}
validationOptionsSet.remove(.issuer)
validator.validationOptionsSet = validationOptionsSet
XCTAssertNoThrow(try validator.isValid(jwts["OktaJWT"] as! String))

// Audience claim
options = [ "audience": "abc123" ]
validator = OktaJWTValidator(options)
XCTAssertThrowsError(try validator.isValid(jwts["OktaJWT"] as! String)) { error in
XCTAssertTrue(error is OktaJWTVerificationError)
XCTAssertEqual(error.localizedDescription, "Token audience does not match the valid audience")
}
validationOptionsSet.remove(.audience)
validator.validationOptionsSet = validationOptionsSet
XCTAssertNoThrow(try validator.isValid(jwts["OktaJWT"] as! String))

// Expiration claim
options = [ "exp": true ]
validator = OktaJWTValidator(options)
XCTAssertThrowsError(try validator.isValid(jwts["OktaJWT"] as! String)) { error in
XCTAssertTrue(error is OktaJWTVerificationError)
XCTAssertEqual(error.localizedDescription, "The JWT expired and is no longer valid")
}
validationOptionsSet.remove(.expiration)
validator.validationOptionsSet = validationOptionsSet
XCTAssertNoThrow(try validator.isValid(jwts["OktaJWT"] as! String))

// IssuedAt claim
options = [ "iat": true,
"leeway": -800000000 ]
validator = OktaJWTValidator(options)
XCTAssertThrowsError(try validator.isValid(jwts["OktaJWT"] as! String)) { error in
XCTAssertTrue(error is OktaJWTVerificationError)
XCTAssertEqual(error.localizedDescription, "The JWT was issued in the future")
}
validationOptionsSet.remove(.issuedAt)
validator.validationOptionsSet = validationOptionsSet
XCTAssertNoThrow(try validator.isValid(jwts["OktaJWT"] as! String))

// Nonce claim
options = [ "nonce": "nonce" ]
validator = OktaJWTValidator(options)
XCTAssertThrowsError(try validator.isValid(jwts["OktaIDToken"] as! String)) { error in
XCTAssertTrue(error is OktaJWTVerificationError)
XCTAssertEqual(error.localizedDescription, "Invalid nonce")
}
validationOptionsSet.remove(.nonce)
validator.validationOptionsSet = validationOptionsSet
XCTAssertNoThrow(try validator.isValid(jwts["OktaIDToken"] as! String))

// Signature
options = [ "issuer": "https://myrealsite.com" ]
validator = OktaJWTValidator(options)
var validationOptions = OktaJWTValidator.ValidationOptions.allOptions
validationOptions.remove(.issuer)
validator.validationOptionsSet = validationOptions
XCTAssertThrowsError(try validator.isValid(jwts["RS512JWT"] as! String)) { error in
XCTAssertTrue(error is OktaAPIError)
}
validationOptionsSet.remove(.signature)
validator.validationOptionsSet = validationOptionsSet
XCTAssertNoThrow(try validator.isValid(jwts["RS512JWT"] as! String))
}
}

#endif