Move credentials classes to the Account framework in NetNewsWire

This commit is contained in:
Maurice Parker 2019-09-08 04:28:43 -05:00
parent 53370ff0d3
commit 7ca2226669
6 changed files with 284 additions and 7 deletions

View File

@ -18,6 +18,9 @@
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; };
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; };
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */; };
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */; };
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */; };
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515E4EB42324FF8C0057B0E7 /* Credentials.swift */; };
5165D7122282080C00D9D53D /* AccountFolderContentsSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */; };
5165D71622821C2400D9D53D /* taggings_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71322821C2400D9D53D /* taggings_delete.json */; };
5165D71722821C2400D9D53D /* taggings_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5165D71422821C2400D9D53D /* taggings_add.json */; };
@ -119,6 +122,9 @@
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = "<group>"; };
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = "<group>"; };
5154367A228EEB28005E1CDF /* FeedbinImportResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinImportResult.swift; sourceTree = "<group>"; };
515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+RSWeb.swift"; sourceTree = "<group>"; };
515E4EB42324FF8C0057B0E7 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
5165D7112282080C00D9D53D /* AccountFolderContentsSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderContentsSyncTest.swift; sourceTree = "<group>"; };
5165D71322821C2400D9D53D /* taggings_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_delete.json; sourceTree = "<group>"; };
5165D71422821C2400D9D53D /* taggings_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = taggings_add.json; sourceTree = "<group>"; };
@ -196,6 +202,16 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
515E4EB12324FF7D0057B0E7 /* Credentials */ = {
isa = PBXGroup;
children = (
515E4EB42324FF8C0057B0E7 /* Credentials.swift */,
515E4EB22324FF8C0057B0E7 /* CredentialsManager.swift */,
515E4EB32324FF8C0057B0E7 /* URLRequest+RSWeb.swift */,
);
path = Credentials;
sourceTree = "<group>";
};
5165D71F22835E9800D9D53D /* FeedFinder */ = {
isa = PBXGroup;
children = (
@ -300,6 +316,7 @@
841974001F6DD1EC006346C4 /* Folder.swift */,
844B297E210CE37E004020B3 /* UnreadCountProvider.swift */,
5165D71F22835E9800D9D53D /* FeedFinder */,
515E4EB12324FF7D0057B0E7 /* Credentials */,
8419742B1F6DDE84006346C4 /* LocalAccount */,
84245C7D1FDDD2580074AFBB /* Feedbin */,
848935031F62484F00CEBD24 /* AccountTests */,
@ -524,9 +541,11 @@
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */,
515E4EB72324FF8C0057B0E7 /* Credentials.swift in Sources */,
51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */,
5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */,
844B297D2106C7EC004020B3 /* Feed.swift in Sources */,
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */,
5154367B228EEB28005E1CDF /* FeedbinImportResult.swift in Sources */,
84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */,
5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */,
@ -542,6 +561,7 @@
5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */,
841974011F6DD1EC006346C4 /* Folder.swift in Sources */,
846E774F1F6EF9C000A165E2 /* LocalAccountDelegate.swift in Sources */,
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */,

View File

@ -0,0 +1,23 @@
//
// Credentials.swift
// NetNewsWire
//
// Created by Brent Simmons on 12/9/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
public enum CredentialsError: Error {
case incompleteCredentials
case unhandledError(status: OSStatus)
}
public enum Credentials {
case basic(username: String, password: String)
case readerAPIBasicLogin(username: String, password: String)
case readerAPIAuthLogin(username: String, apiKey: String)
case oauthAccessToken(username: String, token: String)
case oauthRefreshToken(username: String, token: String)
}

View File

@ -0,0 +1,161 @@
//
// CredentialsManager.swift
// NetNewsWire
//
// Created by Maurice Parker on 5/5/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
public struct CredentialsManager {
public static func storeCredentials(_ credentials: Credentials, server: String) throws {
switch credentials {
case .basic(let username, let password):
try storeBasicCredentials(server: server, username: username, password: password)
case .readerAPIBasicLogin(let username, let password):
try storeBasicCredentials(server: server, username: username, password: password)
case .readerAPIAuthLogin(let username, let apiKey):
try storeBasicCredentials(server: server, username: username, password: apiKey)
case .oauthAccessToken(let username, let token):
try storeBasicCredentials(server: server, username: username, password: token)
case .oauthRefreshToken(let username, let token):
try storeBasicCredentials(server: server, username: username, password: token)
}
}
public static func retrieveBasicCredentials(server: String, username: String) throws -> Credentials? {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
return nil
}
guard status == errSecSuccess else {
throw CredentialsError.unhandledError(status: status)
}
guard let existingItem = item as? [String : Any],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: String.Encoding.utf8) else {
return nil
}
return Credentials.basic(username: username, password: password)
}
public static func removeBasicCredentials(server: String, username: String) throws {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw CredentialsError.unhandledError(status: status)
}
}
public static func retrieveReaderAPIAuthCredentials(server: String, username: String) throws -> Credentials? {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
return nil
}
guard status == errSecSuccess else {
throw CredentialsError.unhandledError(status: status)
}
guard let existingItem = item as? [String : Any],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: String.Encoding.utf8) else {
return nil
}
return Credentials.readerAPIAuthLogin(username: username, apiKey: password)
}
public static func removeReaderAPIAuthCredentials(server: String, username: String) throws {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw CredentialsError.unhandledError(status: status)
}
}
}
// MARK: Private
extension CredentialsManager {
static func storeBasicCredentials(server: String, username: String, password: String) throws {
let passwordData = password.data(using: String.Encoding.utf8)!
let updateQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server]
let attributes: [String: Any] = [kSecValueData as String: passwordData]
let status = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary)
switch status {
case errSecSuccess:
return
case errSecItemNotFound:
break
default:
throw CredentialsError.unhandledError(status: status)
}
guard status == errSecItemNotFound else {
return
}
let addQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecValueData as String: passwordData]
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
if addStatus != errSecSuccess {
throw CredentialsError.unhandledError(status: status)
}
}
}

View File

@ -0,0 +1,64 @@
//
// URLRequest+RSWeb.swift
// NetNewsWire
//
// Created by Brent Simmons on 12/27/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSWeb
public extension URLRequest {
init(url: URL, credentials: Credentials?, conditionalGet: HTTPConditionalGetInfo? = nil) {
self.init(url: url)
guard let credentials = credentials else {
return
}
switch credentials {
case .basic(let username, let password):
let data = "\(username):\(password)".data(using: .utf8)
let base64 = data?.base64EncodedString()
let auth = "Basic \(base64 ?? "")"
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
case .readerAPIBasicLogin(let username, let password):
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
httpMethod = "POST"
let postData = "Email=\(username)&Passwd=\(password)"
httpBody = postData.data(using: String.Encoding.utf8)
case .readerAPIAuthLogin(_, let apiKey):
let auth = "GoogleLogin auth=\(apiKey)"
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
case .oauthAccessToken(_, let token):
let auth = "OAuth \(token)"
setValue(auth, forHTTPHeaderField: "Authorization")
case .oauthRefreshToken:
// While both access and refresh tokens are credentials, it seems the `Credentials` cases
// enumerates how the identity of the user can be proved rather than
// credentials-in-general, such as in this refresh token case,
// the authority to prove an identity.
// TODO: Refactor as usage becomes clearer.
assertionFailure("Refresh tokens are used to replace expired access tokens. Did you mean to use `accessToken` instead?")
break
}
guard let conditionalGet = conditionalGet else {
return
}
// Bug seen in the wild: lastModified with last possible 32-bit date, which is in 2038. Ignore those.
// TODO: drop this check in late 2037.
if let lastModified = conditionalGet.lastModified, !lastModified.contains("2038") {
setValue(lastModified, forHTTPHeaderField: HTTPRequestHeader.ifModifiedSince)
}
if let etag = conditionalGet.etag {
setValue(etag, forHTTPHeaderField: HTTPRequestHeader.ifNoneMatch)
}
}
}

View File

@ -1027,6 +1027,14 @@
name = Products;
sourceTree = "<group>";
};
515E4EA82324FF710057B0E7 /* Credentials */ = {
isa = PBXGroup;
children = (
);
name = Credentials;
path = ../Frameworks/Account/Credentials;
sourceTree = "<group>";
};
5183CCDB226F1EEB0010922C /* Progress */ = {
isa = PBXGroup;
children = (
@ -1579,6 +1587,7 @@
84C9FC6822629C9A00D921D6 /* Shared */ = {
isa = PBXGroup;
children = (
515E4EA82324FF710057B0E7 /* Credentials */,
846E77301F6EF5D600A165E2 /* Account.xcodeproj */,
841D4D542106B3D500DD04E6 /* Articles.xcodeproj */,
841D4D5E2106B3E100DD04E6 /* ArticlesDatabase.xcodeproj */,
@ -1951,12 +1960,12 @@
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
6581C73220CED60000F4AD34 = {
DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
};
840D617B2029031C009BC708 = {
CreatedOnToolsVersion = 9.3;
DevelopmentTeam = M8L2WTLA8W;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
@ -1972,8 +1981,8 @@
};
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.HardenedRuntime = {
enabled = 1;
@ -1982,7 +1991,7 @@
};
849C64701ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = 9C84TZ7Q6Z;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
TestTargetID = 849C645F1ED37A5D003D8FC0;
};

@ -1 +1 @@
Subproject commit bbb58ff2afb539ff65816793754933ae9db8f259
Subproject commit 168ce1a628847d986d032247498b00293e7659f2