From ab48f4a9fa96085afdb1dfb2c65742825d33651b Mon Sep 17 00:00:00 2001 From: Vahan Harutyunyan Date: Thu, 21 Nov 2024 12:09:47 -0500 Subject: [PATCH] Add support for Sign out with ID Token for PingOne Platform --- CHANGELOG.md | 4 ++ FRAuth/FRAuth/Config/FROptions.swift | 8 +++- .../FRAuth/FROptions/FROptionsTests.swift | 42 +++++++++++++++-- .../Discovery/discoveryWithPingEndIdp.json | 46 +++++++++++++++++++ 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/Discovery/discoveryWithPingEndIdp.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 17564768..8e6630f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [4.X.X] +#### Added +- Support Sign out with ID Token for PingOne Platform [SDKS-3424] + ## [4.6.0] #### Added - Support PingOne Protect Marketplace Nodes [SDKS-3296] diff --git a/FRAuth/FRAuth/Config/FROptions.swift b/FRAuth/FRAuth/Config/FROptions.swift index 2c45c2c5..4c3a196d 100644 --- a/FRAuth/FRAuth/Config/FROptions.swift +++ b/FRAuth/FRAuth/Config/FROptions.swift @@ -221,7 +221,11 @@ open class FROptions: NSObject, Codable { self.authorizeEndpoint = config.authorizationEndpoint self.tokenEndpoint = config.tokenEndpoint self.userinfoEndpoint = config.userinfoEndpoint - self.endSessionEndpoint = config.endSessionEndpoint + if let oauthSignoutRedirectUri = oauthSignoutRedirectUri, !oauthSignoutRedirectUri.isEmpty { + self.endSessionEndpoint = config.endSessionEndpoint + } else { + self.endSessionEndpoint = config.pingEndIdpSessionEndpoint ?? config.endSessionEndpoint + } self.revokeEndpoint = config.revocationEndpoint return self @@ -259,6 +263,7 @@ private struct OpenIdConfiguration: Codable { public let userinfoEndpoint: String? public let endSessionEndpoint: String? public let revocationEndpoint: String? + public let pingEndIdpSessionEndpoint: String? private enum CodingKeys: String, CodingKey { @@ -268,6 +273,7 @@ private struct OpenIdConfiguration: Codable { case userinfoEndpoint = "userinfo_endpoint" case endSessionEndpoint = "end_session_endpoint" case revocationEndpoint = "revocation_endpoint" + case pingEndIdpSessionEndpoint = "ping_end_idp_session_endpoint" } } diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FROptions/FROptionsTests.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FROptions/FROptionsTests.swift index 91fa5150..0150a415 100644 --- a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FROptions/FROptionsTests.swift +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FROptions/FROptionsTests.swift @@ -16,7 +16,7 @@ import FRCore class FROptionsTests: FRAuthBaseTest { override func setUp() { - //leave empty + super.setUp() } override func tearDown() { @@ -313,7 +313,7 @@ class FROptionsTests: FRAuthBaseTest { @available(iOS 13.0.0, *) func testDiscoverWithValidURL() async throws { - + FRAuthBaseTest.useMockServer = false var options = FROptions(config: [:]) let validURL = FRTestURL.oidcConfigUrl + "/.well-known/openid-configuration" // Mock network responses as needed @@ -354,7 +354,6 @@ class FROptionsTests: FRAuthBaseTest { XCTAssertEqual(options.revokeEndpoint, FRTestURL.oidcConfigUrl + "/oauth/revoke") } - @available(iOS 13.0.0, *) func testDiscoverWithInvalidURL() async { FRAuthBaseTest.useMockServer = false @@ -368,4 +367,41 @@ class FROptionsTests: FRAuthBaseTest { XCTAssertTrue(["unsupported URL", "OAuth2 /authorize Error"].contains(error.localizedDescription)) } } + + @available(iOS 13.0.0, *) + func testDiscoverEndpointWithPingEndIdpSession() async throws { + FRAuthBaseTest.useMockServer = true + self.loadMockResponses(["discoveryWithPingEndIdp"]) + let config = + ["forgerock_oauth_client_id":"test_client_id", + "forgerock_oauth_redirect_uri": "org.forgerock.demo://oauth2redirect", + "forgerock_oauth_scope" : "openid profile email address" + ] + + var options = FROptions(config: config) + let validURL = FRTestURL.oidcConfigUrl + "/.well-known/openid-configuration" + + //Since config is without oauthSignoutRedirectUri, use ping_end_idp_session_endpoint instead of end_session_endpoint if exists + options = try await options.discover(discoveryURL: validURL) + XCTAssertEqual(options.endSessionEndpoint, "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/idp/signoff") + } + + @available(iOS 13.0.0, *) + func testDiscoverEndpointWithPingEndIdpSessionSignOutRedirect() async throws { + FRAuthBaseTest.useMockServer = true + self.loadMockResponses(["discoveryWithPingEndIdp"]) + let config = + ["forgerock_oauth_client_id":"test_client_id", + "forgerock_oauth_redirect_uri": "org.forgerock.demo://oauth2redirect", + "forgerock_oauth_scope" : "openid profile email address", + "forgerock_oauth_sign_out_redirect_uri": "org.forgerock.demo2://oauth2redirect", + ] + + var options = FROptions(config: config) + let validURL = FRTestURL.oidcConfigUrl + "/.well-known/openid-configuration" + + //Since config is with oauthSignOutRedirectUri, use end_session_endpoint instead of ping_end_idp_session_endpoint + options = try await options.discover(discoveryURL: validURL) + XCTAssertEqual(options.endSessionEndpoint, "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/signoff") + } } diff --git a/FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/Discovery/discoveryWithPingEndIdp.json b/FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/Discovery/discoveryWithPingEndIdp.json new file mode 100644 index 00000000..671827ce --- /dev/null +++ b/FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/Discovery/discoveryWithPingEndIdp.json @@ -0,0 +1,46 @@ +{ + "responsePayload": { + "issuer" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as", + "authorization_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize", + "pushed_authorization_request_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/par", + "token_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/token", + "userinfo_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo", + "jwks_uri" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/jwks", + "end_session_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/signoff", + "ping_end_idp_session_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/idp/signoff", + "check_session_iframe" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/checksession", + "introspection_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/introspect", + "revocation_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/revoke", + "device_authorization_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/device_authorization", + "claims_parameter_supported" : false, + "request_parameter_supported" : true, + "request_uri_parameter_supported" : false, + "require_pushed_authorization_requests" : false, + "scopes_supported" : [ "openid", "profile", "email", "address", "phone", "offline_access" ], + "response_types_supported" : [ "code", "id_token", "token id_token", "code id_token", "code token", "code token id_token" ], + "response_modes_supported" : [ "pi.flow", "query", "fragment", "form_post" ], + "grant_types_supported" : [ "authorization_code", "implicit", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code" ], + "subject_types_supported" : [ "public" ], + "id_token_signing_alg_values_supported" : [ "RS256" ], + "userinfo_signing_alg_values_supported" : [ "none" ], + "request_object_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512" ], + "token_endpoint_auth_methods_supported" : [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ], + "token_endpoint_auth_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512" ], + "claim_types_supported" : [ "normal" ], + "claims_supported" : [ "sub", "iss", "auth_time", "acr", "name", "given_name", "family_name", "middle_name", "preferred_username", "profile", "picture", "zoneinfo", "phone_number", "updated_at", "address", "email", "locale" ], + "code_challenge_methods_supported" : [ "plain", "S256" ] + }, + "response": { + "statusCode": 200, + "url": "http://openam.example.com:8081/openam/.well-known/openid-configuration", + "httpVersion": "HTTP/1.1", + "headerFields": { + "Content-Length": "2", + "Content-Type": "application/json;charset=UTF-8", + "Content-API-Version": "resource=1.0", + "Date": "Tue, 05 May 2020 21:49:02 GMT", + "X-Frame-Options": "SAMEORIGIN", + "X-Content-Type-Options": "nosniff, nosniff" + } + } +}