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

feat: add single sign on feature using SAML #447

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
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
24 changes: 24 additions & 0 deletions Authorization/Authorization.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
0770DE6B28D0C035006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE6D28D0C035006D8A5D /* Localizable.strings */; };
0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE7028D0C0E7006D8A5D /* Strings.swift */; };
5FB79D2802949372CDAF08D6 /* Pods_App_Authorization_AuthorizationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FAE9B7FD61FF88C9C4FE1E8 /* Pods_App_Authorization_AuthorizationTests.framework */; };
99C1654B2C0C4F0600DC384D /* ContainerWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */; };
99C1654D2C0C4F2F00DC384D /* SSOHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */; };
99C1654F2C0C4F5900DC384D /* SSOWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654E2C0C4F5900DC384D /* SSOWebView.swift */; };
99C165512C0C4F7B00DC384D /* SSOWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */; };
BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */; };
BADB3F552AD6DFC3004D5CFA /* SocialAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */; };
DE843D6BB1B9DDA398494890 /* Pods_App_Authorization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47BCFB7C19382EECF15131B6 /* Pods_App_Authorization.framework */; };
Expand Down Expand Up @@ -77,6 +81,10 @@
7A84BB166492D4E46FBCF01C /* Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig"; sourceTree = "<group>"; };
90DFBB75EF40580E180D71C8 /* Pods-App-Authorization.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.debugdev.xcconfig"; sourceTree = "<group>"; };
96C85172770225EB81A6D2DA /* Pods-App-Authorization.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releasedev.xcconfig"; sourceTree = "<group>"; };
99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerWebView.swift; sourceTree = "<group>"; };
99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOHelper.swift; sourceTree = "<group>"; };
99C1654E2C0C4F5900DC384D /* SSOWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOWebView.swift; sourceTree = "<group>"; };
99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOWebViewModel.swift; sourceTree = "<group>"; };
9BF6A1004A955E24527FCF0F /* Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; sourceTree = "<group>"; };
A99D45203C981893C104053A /* Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; sourceTree = "<group>"; };
BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -147,6 +155,7 @@
071009CC28D1E24000344290 /* Presentation */ = {
isa = PBXGroup;
children = (
99C165492C0C4EF000DC384D /* SSO */,
BA8B3A302AD5485100D25EF5 /* SocialAuth */,
E03261622AE6464A002CA7EB /* Startup */,
020C31BD290AADA700D6DEA2 /* Base */,
Expand Down Expand Up @@ -268,6 +277,17 @@
path = ../Pods;
sourceTree = "<group>";
};
99C165492C0C4EF000DC384D /* SSO */ = {
isa = PBXGroup;
children = (
99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */,
99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */,
99C1654E2C0C4F5900DC384D /* SSOWebView.swift */,
99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */,
);
path = SSO;
sourceTree = "<group>";
};
BA8B3A302AD5485100D25EF5 /* SocialAuth */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -502,13 +522,17 @@
02066B462906D72F00F4307E /* SignUpViewModel.swift in Sources */,
E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */,
02A2ACDB2A4B016100FBBBBB /* AuthorizationAnalytics.swift in Sources */,
99C165512C0C4F7B00DC384D /* SSOWebViewModel.swift in Sources */,
99C1654B2C0C4F0600DC384D /* ContainerWebView.swift in Sources */,
025F40E029D1E2FC0064C183 /* ResetPasswordView.swift in Sources */,
020C31CB290BF49900D6DEA2 /* FieldsView.swift in Sources */,
0770DE4E28D0A677006D8A5D /* SignInView.swift in Sources */,
02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */,
99C1654F2C0C4F5900DC384D /* SSOWebView.swift in Sources */,
E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */,
071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */,
BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */,
99C1654D2C0C4F2F00DC384D /* SSOHelper.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import Core

public enum AuthMethod: Equatable {
case password
case SSO
case socailAuth(SocialAuthMethod)

public var analyticsValue: String {
switch self {
case .password:
"password"
case .SSO:
"SSO"
case .socailAuth(let socialAuthMethod):
socialAuthMethod.rawValue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public struct SignInView: View {
.accessibilityIdentifier("password_textfield")
HStack {
if !viewModel.config.features.startupScreenEnabled {
Button(CoreLocalization.register) {
Button(CoreLocalization.SignIn.registerBtn) {
viewModel.router.showRegisterScreen(sourceScreen: viewModel.sourceScreen)
}
.foregroundColor(Theme.Colors.accentColor)
Expand Down Expand Up @@ -155,6 +155,13 @@ public struct SignInView: View {
.frame(maxWidth: .infinity)
.padding(.top, 40)
.accessibilityIdentifier("signin_button")

StyledButton(CoreLocalization.SignIn.logInWithSsoBtn) {
viewModel.router.showSSOWebBrowser(title: CoreLocalization.SignIn.logInBtn)
}
.frame(maxWidth: .infinity)
.padding(.top, 20)
.accessibilityIdentifier("signin_button")
}
}
if viewModel.socialAuthEnabled {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ public class SignInViewModel: ObservableObject {
}
}

@MainActor
func ssoLogin(title: String) async {

analytics.userSignInClicked()
isShowProgress = true
do {
let user = try await interactor.SSOlogin(jwtToken: "239i2oi3jrf2jflkj23lf2f")
analytics.identify(id: "\(user.id)", username: user.username, email: user.email)
analytics.userLogin(method: .password)
router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen)
} catch let error {
failure(error)
}
}

@MainActor
func login(with result: Result<SocialAuthDetails, Error>) async {
switch result {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public struct SignUpView: View {
VStack(alignment: .center) {
ZStack {
HStack {
Text(CoreLocalization.register)
Text(CoreLocalization.SignIn.registerBtn)
.titleSettings(color: Theme.Colors.loginNavigationText)
.accessibilityIdentifier("register_text")
}
Expand All @@ -64,7 +64,7 @@ public struct SignUpView: View {
ScrollView {
VStack(alignment: .leading) {

Text(CoreLocalization.register)
Text(CoreLocalization.SignIn.registerBtn)
.font(Theme.Fonts.displaySmall)
.foregroundColor(Theme.Colors.textPrimary)
.padding(.bottom, 4)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// ContainerWebView.swift
// Authorization
//
// Created by Rawan Matar on 02/06/2024.
//

import SwiftUI
import Core
import Swinject

public struct ContainerWebView: View {

// MARK: - Internal Properties

let url: String
private var pageTitle: String
@Environment(\.presentationMode) var presentationMode

// MARK: - Init

public init(_ url: String, title: String) {
self.url = url
self.pageTitle = title
}

// MARK: - UI

public var body: some View {
VStack(alignment: .center) {
NavigationBar(
title: pageTitle,
leftButtonAction: { presentationMode.wrappedValue.dismiss() }
)

ZStack {
if !url.isEmpty {
SSOWebView(url: URL(string: url), viewModel: Container.shared.resolve(SSOWebViewModel.self)!)
} else {
EmptyView()
}
}
.accessibilityIdentifier("web_browser")
}
}
}
73 changes: 73 additions & 0 deletions Authorization/Authorization/Presentation/SSO/SSOHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// SSOHelper.swift
// Authorization
//
// Created by Rawan Matar on 02/06/2024.
//

import Foundation

// https://developer.apple.com/documentation/ios-ipados-release-notes/foundation-release-notes

/**
A Helper for some of the SSO preferences.
Keeps data under the UserDefaults.
*/
public class SSOHelper: NSObject {

public enum UserDefaultKeys: String, CaseIterable {
case cookiePayload
case cookieSignature
case userInfo

var description: String {
switch self {
case .cookiePayload:
return "edx-jwt-cookie-header-payload"
case .cookieSignature:
return "edx-jwt-cookie-signature"
case .userInfo:
return "edx-user-info"
}
}
}

// MARK: - Singleton

public static let shared = SSOHelper()

// MARK: - Public Properties

/// Authentication
public var cookiePayload: String? {
get {
let defaults = UserDefaults.standard
return defaults.string(forKey: UserDefaultKeys.cookiePayload.rawValue)
}
set (newValue) {
let defaults = UserDefaults.standard
defaults.set(newValue, forKey: UserDefaultKeys.cookiePayload.rawValue)
}
}

/// Authentication
public var cookieSignature: String? {
get {
let defaults = UserDefaults.standard
return defaults.string(forKey: UserDefaultKeys.cookieSignature.rawValue)
}
set (newValue) {
let defaults = UserDefaults.standard
defaults.set(newValue, forKey: UserDefaultKeys.cookieSignature.rawValue)
}
}

// MARK: - Public Methods

/// Checks if the user is login.
public func cleanAfterSuccesfulLogout() {
cookiePayload = nil
cookieSignature = nil
}
}

104 changes: 104 additions & 0 deletions Authorization/Authorization/Presentation/SSO/SSOWebView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// SSOWebView.swift
// Authorization
//
// Created by Rawan Matar on 02/06/2024.
//


import SwiftUI
import WebKit
import Core

public struct SAMLConstants {

public init() {}

public let samlLoginSuccess = URL(string: "https://blue.zeitlabs.com/auth/complete/tpa-saml")!
}

public struct SSOWebView: UIViewRepresentable {

let url: URL?

var viewModel: SSOWebViewModel

public init(url: URL?, viewModel: SSOWebViewModel) {
self.url = url
self.viewModel = viewModel
}

public func makeUIView(context: Context) -> WKWebView {
let coordinator = makeCoordinator()
let userContentController = WKUserContentController()
userContentController.add(coordinator, name: "bridge")

let prefs = WKWebpagePreferences()
let config = WKWebViewConfiguration()
prefs.allowsContentJavaScript = true

config.userContentController = userContentController
config.defaultWebpagePreferences = prefs
config.websiteDataStore = WKWebsiteDataStore.nonPersistent()

let wkWebView = WKWebView(frame: .zero, configuration: config)
wkWebView.navigationDelegate = coordinator

guard let currentURL = url else {
return wkWebView
}
let request = URLRequest(url: currentURL)
wkWebView.load(request)

return wkWebView
}

public func updateUIView(_ uiView: WKWebView, context: Context) {

}

public func makeCoordinator() -> Coordinator {
Coordinator(viewModel: self.viewModel)
}

public class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
var viewModel: SSOWebViewModel

init(viewModel: SSOWebViewModel) {
self.viewModel = viewModel
super.init()
}

// WKScriptMessageHandler
public func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
}

// WKNavigationDelegate
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let _ = webView.url?.absoluteString else {
return
}

}

public func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = webView.url?.absoluteString else {
decisionHandler(.allow)
return
}

if url.contains(SAMLConstants().samlLoginSuccess.absoluteString) {
webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
Task {
await self.viewModel.SSOLogin(cookies: cookies)
}
}
}

decisionHandler(.allow)
}
}
}
Loading
Loading