Skip to content
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
34 changes: 21 additions & 13 deletions Authenticator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
6FAFAD3E277FCD590041F854 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FAFAD3D277FCD590041F854 /* AppDelegate.swift */; };
6FAFAD91277FCD590041F854 /* DriveAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FAFAD90277FCD590041F854 /* DriveAuth.swift */; };
6FAFAD41277FCD590041F854 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FAFAD3F277FCD590041F854 /* Main.storyboard */; };
6FAFAD53277FCD590041F854 /* Authenticator Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6FAFAD52277FCD590041F854 /* Authenticator Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
6FAFAD5A277FCD590041F854 /* Main.html in Resources */ = {isa = PBXBuildFile; fileRef = 6FAFAD1C277FCD550041F854 /* Main.html */; };
Expand Down Expand Up @@ -62,21 +63,22 @@
6FAFAD24277FCD590041F854 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
6FAFAD3B277FCD590041F854 /* Authenticator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Authenticator.app; sourceTree = BUILT_PRODUCTS_DIR; };
6FAFAD3D277FCD590041F854 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
6FAFAD90277FCD590041F854 /* DriveAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DriveAuth.swift; sourceTree = "<group>"; };
6FAFAD40277FCD590041F854 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
6FAFAD42277FCD590041F854 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6FAFAD43277FCD590041F854 /* Authenticator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Authenticator.entitlements; sourceTree = "<group>"; };
6FAFAD52277FCD590041F854 /* Authenticator Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Authenticator Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
6FAFAD57277FCD590041F854 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6FAFAD58277FCD590041F854 /* Authenticator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Authenticator.entitlements; sourceTree = "<group>"; };
6FAFAD78277FCD5B0041F854 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; name = LICENSE; path = ./Authenticator/test/chrome/LICENSE; sourceTree = "<group>"; };
6FAFAD79277FCD5B0041F854 /* css */ = {isa = PBXFileReference; lastKnownFileType = folder; name = css; path = ./Authenticator/test/chrome/css; sourceTree = "<group>"; };
6FAFAD7A277FCD5B0041F854 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dist; path = ./Authenticator/test/chrome/dist; sourceTree = "<group>"; };
6FAFAD7B277FCD5B0041F854 /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; name = images; path = ./Authenticator/test/chrome/images; sourceTree = "<group>"; };
6FAFAD7C277FCD5B0041F854 /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = ./Authenticator/test/chrome/manifest.json; sourceTree = "<group>"; };
6FAFAD7D277FCD5B0041F854 /* view */ = {isa = PBXFileReference; lastKnownFileType = folder; name = view; path = ./Authenticator/test/chrome/view; sourceTree = "<group>"; };
6FAFAD7E277FCD5B0041F854 /* schema.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = schema.json; path = ./Authenticator/test/chrome/schema.json; sourceTree = "<group>"; };
6FAFAD7F277FCD5B0041F854 /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; name = _locales; path = ./Authenticator/test/chrome/_locales; sourceTree = "<group>"; };
6FAFAD80277FCD5B0041F854 /* manifest-pwa.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = "manifest-pwa.json"; path = "./Authenticator/test/chrome/manifest-pwa.json"; sourceTree = "<group>"; };
6FAFAD78277FCD5B0041F854 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; name = LICENSE; path = ../authenticator/safari/LICENSE; sourceTree = "<group>"; };
6FAFAD79277FCD5B0041F854 /* css */ = {isa = PBXFileReference; lastKnownFileType = folder; name = css; path = ../authenticator/safari/css; sourceTree = "<group>"; };
6FAFAD7A277FCD5B0041F854 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dist; path = ../authenticator/safari/dist; sourceTree = "<group>"; };
6FAFAD7B277FCD5B0041F854 /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; name = images; path = ../authenticator/safari/images; sourceTree = "<group>"; };
6FAFAD7C277FCD5B0041F854 /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = ../authenticator/safari/manifest.json; sourceTree = "<group>"; };
6FAFAD7D277FCD5B0041F854 /* view */ = {isa = PBXFileReference; lastKnownFileType = folder; name = view; path = ../authenticator/safari/view; sourceTree = "<group>"; };
6FAFAD7E277FCD5B0041F854 /* schema.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = schema.json; path = ../authenticator/safari/schema.json; sourceTree = "<group>"; };
6FAFAD7F277FCD5B0041F854 /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; name = _locales; path = ../authenticator/safari/_locales; sourceTree = "<group>"; };
6FAFAD80277FCD5B0041F854 /* manifest-pwa.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = "manifest-pwa.json"; path = "../authenticator/safari/manifest-pwa.json"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -151,6 +153,7 @@
isa = PBXGroup;
children = (
6FAFAD3D277FCD590041F854 /* AppDelegate.swift */,
6FAFAD90277FCD590041F854 /* DriveAuth.swift */,
6FAFAD3F277FCD590041F854 /* Main.storyboard */,
6FAFAD42277FCD590041F854 /* Info.plist */,
6FAFAD43277FCD590041F854 /* Authenticator.entitlements */,
Expand Down Expand Up @@ -299,6 +302,7 @@
files = (
6FAFAD62277FCD590041F854 /* ViewController.swift in Sources */,
6FAFAD3E277FCD590041F854 /* AppDelegate.swift in Sources */,
6FAFAD91277FCD590041F854 /* DriveAuth.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -467,7 +471,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 14.4;
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-framework",
Expand Down Expand Up @@ -498,7 +502,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 14.4;
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-framework",
Expand Down Expand Up @@ -532,9 +536,11 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 14.4;
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-framework",
AuthenticationServices,
"-framework",
SafariServices,
"-framework",
Expand Down Expand Up @@ -567,9 +573,11 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 14.4;
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-framework",
AuthenticationServices,
"-framework",
SafariServices,
"-framework",
Expand Down
105 changes: 91 additions & 14 deletions Shared (Extension)/SafariWebExtensionHandler.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,103 @@
//
// SafariWebExtensionHandler.swift
// Shared (Extension)
//
// Created by penlv on 12/31/21.
//

import SafariServices
import AppKit
import os.log
import SafariServices

let SFExtensionMessageKey = "message"

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
private let appGroupId = "group.com.yourCompany.Authenticator"
private let driveAuthNotification = Notification.Name("com.yourCompany.Authenticator.driveAuth")

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
let item = context.inputItems[0] as! NSExtensionItem
let message = item.userInfo?[SFExtensionMessageKey]
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
DispatchQueue.main.async {
self.handle(context)
}
}

let response = NSExtensionItem()
response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
private func handle(_ context: NSExtensionContext) {
let item = context.inputItems.first as? NSExtensionItem
let message = item?.userInfo?[SFExtensionMessageKey] as? [String: Any]
os_log(.default, "Received native message")

guard
message?["action"] as? String == "drive-auth",
let urlString = message?["url"] as? String,
let url = URL(string: urlString),
url.scheme == "https",
url.host == "accounts.google.com"
else {
complete(context, ["ok": false, "error": "unsupported"])
return
}

guard let defaults = UserDefaults(suiteName: appGroupId) else {
complete(context, ["ok": false, "error": "failed"])
return
}

defaults.removeObject(forKey: "driveAuthCallback")
defaults.removeObject(forKey: "driveAuthError")
defaults.set(urlString, forKey: "driveAuthURL")
defaults.set("pending", forKey: "driveAuthState")
defaults.synchronize()

openContainerApp()
DistributedNotificationCenter.default().post(name: driveAuthNotification, object: nil)
poll(context: context, defaults: defaults, deadline: Date().addingTimeInterval(180))
}

private func poll(
context: NSExtensionContext,
defaults: UserDefaults,
deadline: Date
) {
let state = defaults.string(forKey: "driveAuthState")
if state == "done", let callback = defaults.string(forKey: "driveAuthCallback") {
clear(defaults)
complete(context, ["ok": true, "url": callback])
return
}
if state == "error" {
let error = defaults.string(forKey: "driveAuthError") ?? "failed"
clear(defaults)
complete(context, ["ok": false, "error": error])
return
}
if Date() > deadline {
clear(defaults)
complete(context, ["ok": false, "error": "timeout"])
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
self.poll(context: context, defaults: defaults, deadline: deadline)
}
}

private func clear(_ defaults: UserDefaults) {
defaults.removeObject(forKey: "driveAuthCallback")
defaults.removeObject(forKey: "driveAuthError")
defaults.removeObject(forKey: "driveAuthURL")
defaults.set("idle", forKey: "driveAuthState")
defaults.synchronize()
}

private func complete(_ context: NSExtensionContext, _ body: [String: Any]) {
let response = NSExtensionItem()
response.userInfo = [SFExtensionMessageKey: body]
context.completeRequest(returningItems: [response], completionHandler: nil)
}

private func openContainerApp() {
let appURL = Bundle.main.bundleURL
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let config = NSWorkspace.OpenConfiguration()
config.activates = true
NSWorkspace.shared.openApplication(at: appURL, configuration: config) { _, error in
if error != nil {
os_log(.error, "Could not open Authen")
}
}
}
}
2 changes: 1 addition & 1 deletion authenticator
Submodule authenticator updated 124 files
9 changes: 8 additions & 1 deletion macOS (App)/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {

func applicationDidFinishLaunching(_ notification: Notification) {
// Override point for customization after application launch.
DriveAuth.shared.startObserving()
DispatchQueue.main.async {
DriveAuth.shared.startIfPending()
}
}

func applicationDidBecomeActive(_ notification: Notification) {
DriveAuth.shared.startIfPending()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
Expand Down
78 changes: 78 additions & 0 deletions macOS (App)/DriveAuth.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import AuthenticationServices
import Cocoa

private let appGroupId = "group.com.yourCompany.Authenticator"
private let driveAuthNotification = Notification.Name("com.yourCompany.Authenticator.driveAuth")
private let driveRedirect = URL(string: "https://authenticator.cc/oauth")!

final class DriveAuth: NSObject, ASWebAuthenticationPresentationContextProviding {
static let shared = DriveAuth()

private var session: ASWebAuthenticationSession?

func startObserving() {
DistributedNotificationCenter.default().addObserver(
forName: driveAuthNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.startIfPending()
}
}

func startIfPending() {
guard session == nil else {
return
}
guard let defaults = UserDefaults(suiteName: appGroupId) else {
return
}
guard defaults.string(forKey: "driveAuthState") == "pending",
let urlString = defaults.string(forKey: "driveAuthURL"),
let url = URL(string: urlString),
url.scheme == "https",
url.host == "accounts.google.com"
else {
return
}

let session = ASWebAuthenticationSession(
url: url,
callback: .https(host: driveRedirect.host ?? "authenticator.cc", path: driveRedirect.path),
completionHandler: { [weak self] callbackURL, error in
self?.finish(callbackURL: callbackURL, error: error)
}
)
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = false
self.session = session
if !session.start() {
finish(callbackURL: nil, error: nil)
}
}

func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return NSApplication.shared.keyWindow
?? NSApplication.shared.windows.first
?? ASPresentationAnchor()
}

private func finish(callbackURL: URL?, error: Error?) {
let defaults = UserDefaults(suiteName: appGroupId)
if let callbackURL = callbackURL,
callbackURL.scheme == "https",
callbackURL.host == driveRedirect.host,
callbackURL.path == driveRedirect.path
{
defaults?.set(callbackURL.absoluteString, forKey: "driveAuthCallback")
defaults?.set("done", forKey: "driveAuthState")
} else {
let cancelled = (error as NSError?)?.code == ASWebAuthenticationSessionError.canceledLogin.rawValue
defaults?.set(cancelled ? "cancelled" : "failed", forKey: "driveAuthError")
defaults?.set("error", forKey: "driveAuthState")
}
defaults?.removeObject(forKey: "driveAuthURL")
defaults?.synchronize()
session = nil
}
}
36 changes: 30 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
# Authen

Safari extension distro for [Authenticator](https://github.com/Authenticator-Extension/Authenticator)
Safari extension distro for [Authenticator](https://github.com/Authenticator-Extension/Authenticator)

<a href="https://apps.apple.com/us/app/authen/id1602945200?mt=12" target="_blank"><img width="150" alt="Download on the App Store" src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg"/></a>

![2560x1600bb](https://user-images.githubusercontent.com/876920/176361939-a359dd46-254a-4784-86bf-19f9c999f5aa.png)

## How to contribute
## Build

* Clone the project
* `git submodule update --remote` to update the sub module for [Authenticator](https://github.com/Authenticator-Extension/Authenticator)
* `npm ci && npm run chrome`
* Launch the project and run
The extension lives in the `authenticator` submodule.

```
git submodule update --init
./scripts/build-safari.sh
```

Open `Authenticator.xcodeproj` and run the macOS app. The script installs the submodule dependencies with Puppeteer's Chromium download skipped, and it sets `NODE_OPTIONS=--openssl-legacy-provider` so webpack 4 runs on current Node. Xcode packages `authenticator/safari`.

The bundle ids stay `com.yourCompany.Authenticator` and `com.yourCompany.Authenticator.Extension`. `DEVELOPMENT_TEAM` is empty. Signing and the App Store upload stay with the maintainers. The deployment target is macOS 14.4, which is what `ASWebAuthenticationSession` needs for an https callback.

Google Drive sign-in uses the client id already in the Chrome manifest. The client secret in `authenticator/src/models/credentials.ts` stays empty in git. The release script fills it before a production build. The redirect is `https://authenticator.cc/oauth`.

## Drive backups

A backup is a dated JSON file in a Drive folder named Authenticator Backups. The scope is `drive.file`, so the app only sees files it created. There is no download or merge.

With a passphrase, the file is encrypted. That encryption is only as strong as the passphrase. With encryption turned off, or with no passphrase, the file is the raw secrets. The Drive page already warns about that. Dropbox and OneDrive stay in the code and stay hidden on Safari.

## Dependencies

`crypto-js` is 4.2.0. That clears the production PBKDF2 advisory `npm audit` reported for 4.0.0.

These stay unfixed because the fix is a new major version, and this tree stays on Vue 2 and webpack 4:

- `uuid` 3.x. The patched release is 11.
- Vue 2. The ReDoS advisory is fixed in Vue 3, and Vue 2 is end of life.
- High and critical findings in the dev tree (webpack 4, eslint 6, and their dependencies). `npm audit fix --force` would leave Vue 2 and webpack 4.
16 changes: 16 additions & 0 deletions scripts/build-safari.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT/authenticator"

case ":${NODE_OPTIONS:-}:" in
*--openssl-legacy-provider*) ;;
*) export NODE_OPTIONS="${NODE_OPTIONS:-} --openssl-legacy-provider" ;;
esac

if [[ ! -d node_modules ]]; then
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm ci
fi

npm run safari