Skip to content

Commit

Permalink
add sso endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
KaterinaWire committed Feb 17, 2025
1 parent fb85a24 commit dfa2732
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@ public protocol AuthenticationAPI {

func getDomainRegistration(forEmail email: String) async throws -> DomainRegistrationConfiguration

/// Generate the link to the SSO authentication screen
///
/// - Parameters:
/// - baseURL: backend URL
/// - ssoCode: SSO code
/// - callbackScheme: the URL scheme that where the callback will be provided
/// - Returns: URL to the SSO authentication screen

func buildSSOLink(baseURL: URL, ssoCode: UUID, callbackScheme: String) async throws -> URL

/// Get the default SSO code associated with the backend

func getSSOCode() async throws -> UUID?

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,40 @@ public enum AuthenticationAPIError: Error {
case invalidCredentials

}

extension AuthenticationAPIError {

typealias StatusCode = Int

// enum SSOLogin: Error {
//
// case invalidSSOCode
//
// case invalidStatus(StatusCode)
//
// case unknown
//
// }

enum SSOLoginError: Equatable, Error {

case invalidSSOCode

case invalidStatus(StatusCode)

case unknown


init?(response: HTTPURLResponse) {
switch (response.statusCode) {
case 404:
self = .invalidSSOCode
case (400 ... 599):
self = .invalidStatus(response.statusCode)
default:
return nil
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//

import Foundation
//import WireCommonComponents

class AuthenticationAPIV0: AuthenticationAPI, VersionedAPI {
let apiService: any APIServiceProtocol
Expand Down Expand Up @@ -139,4 +140,96 @@ class AuthenticationAPIV0: AuthenticationAPI, VersionedAPI {
func getDomainRegistration(forEmail email: String) async throws -> DomainRegistrationConfiguration {
throw AuthenticationAPIError.unsupportedEndpointForAPIVersion
}

func buildSSOLink(baseURL: URL, ssoCode: UUID, callbackScheme: String) async throws -> URL {
let path = "/sso/initiate-login/\(ssoCode.uuidString)"
let requestBuilder = try URLRequestBuilder(path: path)
.withMethod(.head)
.resolvingAgainst(baseURL: baseURL)

let request = requestBuilder.build()
do {
try await validateLoginToken(request: request)

let successCallback = makeSuccessCallbackString(using: ssoCode, callbackScheme: callbackScheme)
let errorCallback = makeFailureCallbackString(using: ssoCode, callbackScheme: callbackScheme)

let url = requestBuilder
.withQueryItem(name: URLQueryItem.Key.successRedirect, value: successCallback)
.withQueryItem(name: URLQueryItem.Key.errorRedirect, value: errorCallback)
.build().url

guard let url else {
throw AuthenticationAPIError.SSOLoginError.invalidSSOCode
}

return url
} catch {
throw error
}
}

// Try the request to test validity.
private func validateLoginToken(request: URLRequest) async throws {
do {
let (_, response) = try await URLSession(configuration: .ephemeral).data(for: request)
guard let response = response as? HTTPURLResponse else {
throw AuthenticationAPIError.SSOLoginError.unknown
}

if let validationError = AuthenticationAPIError.SSOLoginError(response: response) {
throw validationError
}
} catch {
throw error
}
}

private func makeSuccessCallbackString(using token: UUID, callbackScheme: String) -> String {
var components = URLComponents()
components.scheme = callbackScheme
components.host = URL.Host.login
components.path = "/" + URL.Path.success

components.queryItems = [
URLQueryItem(name: URLQueryItem.Key.cookie, value: URLQueryItem.Template.cookie),
URLQueryItem(name: URLQueryItem.Key.userIdentifier, value: URLQueryItem.Template.userIdentifier),
URLQueryItem(name: URLQueryItem.Key.validationToken, value: token.transportString())
]

return components.url!.absoluteString
}

private func makeFailureCallbackString(using token: UUID, callbackScheme: String) -> String {
var components = URLComponents()
components.scheme = callbackScheme
components.host = URL.Host.login
components.path = "/" + URL.Path.failure

components.queryItems = [
URLQueryItem(name: URLQueryItem.Key.errorLabel, value: URLQueryItem.Template.errorLabel),
URLQueryItem(name: URLQueryItem.Key.validationToken, value: token.transportString())
]

return components.url!.absoluteString
}

func getSSOCode() async throws -> UUID? {
let path = "/sso/settings"
let request = try URLRequestBuilder(path: path)
.withMethod(.get)
.withAcceptType(.json)
.build()

let (data, response) = try await apiService.executeRequest(
request,
requiringAccessToken: false
)

let payload = try ResponseParser()
.success(code: .ok, type: SSOSettingsResponseV0.self)
.parse(code: response.statusCode, data: data)

return payload.defaultSSOCode
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation

extension URL {

enum Host {
static let login = "login"
}

enum Path {
static let success = "success"
static let failure = "failure"
}

}

extension URLQueryItem {

enum Key {
static let successRedirect = "success_redirect"
static let errorRedirect = "error_redirect"
static let cookie = "cookie"
static let userIdentifier = "userid"
static let errorLabel = "label"
static let validationToken = "validation_token"
}

enum Template {
static let cookie = "$cookie"
static let userIdentifier = "$userid"
static let errorLabel = "$label"
}

}

//public extension Bundle {
// static var ssoURLScheme: String? {
// Bundle.appMainBundle.infoForKey("Wire SSO URL Scheme")
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation

struct SSOSettingsResponseV0: Decodable, ToAPIModelConvertible {

let defaultSSOCode: UUID?

private enum CodingKeys: String, CodingKey {
case defaultSSOCode = "default_sso_code"
}

func toAPIModel() -> SSOSettings {
.init(defaultSSOCode: defaultSSOCode)
}

}
25 changes: 25 additions & 0 deletions WireAPI/Sources/WireAPI/Models/Authorization/SSOSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation

public struct SSOSettings: Equatable, Sendable {

public let defaultSSOCode: UUID?

}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ struct URLRequestBuilder {
}
}

func resolvingAgainst(baseURL: URL) -> Self {
withCopy {
guard let relativePath = request.url?.relativeString else { return }

var components = URLComponents()
components.scheme = "https"
components.host = baseURL.host
components.path = relativePath

guard let resolvedURL = components.url else { return }
$0.request.url = resolvedURL
}
}

private func withCopy(_ mutation: (inout Self) -> Void) -> Self {
var copy = self
mutation(&copy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,12 @@ extension CompanyLoginController {
/// Attemts to login with a SSO login code.
///
/// - Parameter code: The SSO team code that was extracted from the link.
func attemptLoginWithSSOCode(_ code: UUID) {
func attemptLoginWithSSOCode(_ code: UUID) {//
guard !presentOfflineAlertIfNeeded() else { return }

delegate?.controller(self, showLoadingView: true)

let host = BackendEnvironment.shared.backendURL.host!
let host = BackendEnvironment.shared.backendURL.host!//
requester.validate(host: host, token: code) {
self.delegate?.controller(self, showLoadingView: false)
guard !self.handleValidationErrorIfNeeded($0) else { return }
Expand Down

0 comments on commit dfa2732

Please sign in to comment.