diff --git a/kotlin/src/main/com/looker/rtl/OAuthSession.kt b/kotlin/src/main/com/looker/rtl/OAuthSession.kt index 875b37cc5..0a5e7ab6d 100644 --- a/kotlin/src/main/com/looker/rtl/OAuthSession.kt +++ b/kotlin/src/main/com/looker/rtl/OAuthSession.kt @@ -27,77 +27,10 @@ package com.looker.rtl import com.looker.sdk.AccessToken import java.security.MessageDigest import java.security.SecureRandom -import kotlin.experimental.and +import java.util.Base64 -// https://stackoverflow.com/a/52225984/74137 -// TODO performance comparison of these two methods -@ExperimentalUnsignedTypes -fun ByteArray.toHexStr() = asUByteArray().joinToString("") { it.toString(16).padStart(2, '0') } - -// Adapted from https://www.samclarke.com/kotlin-hash-strings/ - -fun String.md5(): String { - return hashString(this, "MD5") -} - -fun String.sha512(): String { - return hashString(this, "SHA-512") -} - -fun String.sha256(): String { - return hashString(this, "SHA-256") -} - -fun String.sha1(): String { - return hashString(this, "SHA-1") -} - -fun hashString(input: ByteArray, digester: MessageDigest): String { - val HEX_CHARS = "0123456789abcdef" - val bytes = digester - .digest(input) - val result = StringBuilder(bytes.size * 2) - - bytes.forEach { - val i = it.toInt() - result.append(HEX_CHARS[i shr 4 and 0x0f]) - result.append(HEX_CHARS[i and 0x0f]) - } - - return result.toString() -} - -fun hashString(input: ByteArray, type: String): String { - val digester = MessageDigest.getInstance(type) - return hashString(input, digester) -} - -/** - * Supported algorithms on Android: - * - * Algorithm Supported API Levels - * MD5 1+ - * SHA-1 1+ - * SHA-224 1-8,22+ - * SHA-256 1+ - * SHA-384 1+ - * SHA-512 1+ - */ -fun hashString(input: String, type: String): String { - return hashString(input.toByteArray(), type) -} - -private val hexArray = "0123456789abcdef".toCharArray() - -fun hexStr(bytes: ByteArray): String { - val hexChars = CharArray(bytes.size * 2) - for (j in bytes.indices) { - val v = (bytes[j] and 0xFF.toByte()).toInt() - - hexChars[j * 2] = hexArray[v ushr 4] - hexChars[j * 2 + 1] = hexArray[v and 0x0F] - } - return String(hexChars) +fun base64UrlEncode(bytes: ByteArray): String { + return Base64.getUrlEncoder().encodeToString(bytes) } @ExperimentalUnsignedTypes @@ -142,7 +75,7 @@ class OAuthSession(override val apiSettings: ConfigurationProvider, override val * Generate an OAuth2 authCode request URL */ fun createAuthCodeRequestUrl(scope: String, state: String): String { - this.codeVerifier = this.secureRandom(32).toHexStr() + this.codeVerifier = base64UrlEncode(this.secureRandom(32)) val codeChallenge = this.sha256hash(this.codeVerifier) val config = this.apiSettings.readConfig() val lookerUrl = config["looker_url"] @@ -160,14 +93,13 @@ class OAuthSession(override val apiSettings: ConfigurationProvider, override val fun redeemAuthCodeBody(authCode: String, codeVerifier: String? = null): Map { val verifier = codeVerifier?: this.codeVerifier val config = this.apiSettings.readConfig() - val map = mapOf( + return mapOf( "grant_type" to "authorization_code", "code" to authCode, "code_verifier" to verifier, "client_id" to (config["client_id"] ?: error("")), "redirect_uri" to (config["redirect_uri"] ?: error("")) ) - return map } fun redeemAuthCode(authCode: String, codeVerifier: String? = null): AuthToken { @@ -181,7 +113,8 @@ class OAuthSession(override val apiSettings: ConfigurationProvider, override val } fun sha256hash(value: ByteArray): String { - return hashString(value, messageDigest) + val bytes = messageDigest.digest(value) + return base64UrlEncode(bytes) } fun sha256hash(value: String): String { diff --git a/kotlin/src/test/TestAuthSession.kt b/kotlin/src/test/TestAuthSession.kt index fd91c8844..5da504e77 100644 --- a/kotlin/src/test/TestAuthSession.kt +++ b/kotlin/src/test/TestAuthSession.kt @@ -76,13 +76,9 @@ class TestAuthSession { @test fun testSha256() { val session = OAuthSession(settings, Transport(testSettings)) - val rosettaCode = "Rosetta code" - val rosettaHash = "764faf5c61ac315f1497f9dfa542713965b785e5cc2f707d6468d7d1124cdfcf" - var hash = session.sha256hash(rosettaCode) - assertEquals(rosettaHash, hash, "Rosetta code should match") val message = "The quick brown fox jumped over the lazy dog." - hash = session.sha256hash(message) - assertEquals("68b1282b91de2c054c36629cb8dd447f12f096d3e3c587978dc2248444633483", hash, "Quick brown fox should match") + val hash = session.sha256hash(message) + assertEquals("aLEoK5HeLAVMNmKcuN1EfxLwltPjxYeXjcIkhERjNIM=", hash, "Quick brown fox should match") } @test diff --git a/python/looker_sdk/rtl/auth_session.py b/python/looker_sdk/rtl/auth_session.py index 7a5a1fe1c..3b733e7f4 100644 --- a/python/looker_sdk/rtl/auth_session.py +++ b/python/looker_sdk/rtl/auth_session.py @@ -223,7 +223,7 @@ def _ok(self, response: transport.Response) -> str: class CryptoHash: def secure_random(self, byte_count: int) -> str: - return secrets.token_hex() + return secrets.token_urlsafe(byte_count) def sha256_hash(self, message: str) -> str: value = hashlib.sha256() diff --git a/swift/looker/Tests/lookerTests/authSessionTests.swift b/swift/looker/Tests/lookerTests/authSessionTests.swift index 8ed912a28..2a43c5121 100644 --- a/swift/looker/Tests/lookerTests/authSessionTests.swift +++ b/swift/looker/Tests/lookerTests/authSessionTests.swift @@ -41,7 +41,7 @@ class authSessionTests: XCTestCase { let session = OAuthSession(settings, xp) let message = "The quick brown fox jumped over the lazy dog." let hash = session.sha256Hash(message) - XCTAssertEqual("68b1282b91de2c054c36629cb8dd447f12f096d3e3c587978dc2248444633483", hash) + XCTAssertEqual("aLEoK5HeLAVMNmKcuN1EfxLwltPjxYeXjcIkhERjNIM", hash) } func testRedemptionBody() { diff --git a/swift/looker/Tests/lookerTests/methodsTests.swift b/swift/looker/Tests/lookerTests/methodsTests.swift index cae4bc082..75dc22318 100644 --- a/swift/looker/Tests/lookerTests/methodsTests.swift +++ b/swift/looker/Tests/lookerTests/methodsTests.swift @@ -97,7 +97,7 @@ class methodsTests: XCTestCase { XCTAssertNotNil(me) _ = sdk.authSession.logout() } - + func testUserSearch() { let list = try? sdk.ok(sdk.search_users( first_name:"%", diff --git a/swift/looker/rtl/apiConfig.swift b/swift/looker/rtl/apiConfig.swift index c6c12ab34..47ab4e980 100644 --- a/swift/looker/rtl/apiConfig.swift +++ b/swift/looker/rtl/apiConfig.swift @@ -79,7 +79,7 @@ public func parseConfig(_ filename : String) -> Config { return config } -public class ApiConfig: IApiSettings { +open class ApiConfig: IApiSettings { public func readConfig(_ section: String? = nil) -> IApiSection { if (self.fileName == "") { // No config file to read @@ -103,16 +103,16 @@ public class ApiConfig: IApiSettings { private var fileName = "" private var section = "Looker" - init() { + public init() { self.assign(DefaultSettings()) } - init(_ settings: IApiSettings) { + public init(_ settings: IApiSettings) { self.assign(settings) } /// Get SDK settings from a configuration file with environment variable overrides - init(_ fileName: String = "", _ section: String = "Looker") throws { + public init(_ fileName: String = "", _ section: String = "Looker") throws { let fm = FileManager.default if (fileName == "") { // Default file name to looker.ini? diff --git a/swift/looker/rtl/apiMethods.swift b/swift/looker/rtl/apiMethods.swift index f3579d2bf..8af2020e8 100644 --- a/swift/looker/rtl/apiMethods.swift +++ b/swift/looker/rtl/apiMethods.swift @@ -29,7 +29,7 @@ open class APIMethods { public var authSession: IAuthorizer public var encoder = JSONEncoder() - init(_ authSession: IAuthorizer) { + public init(_ authSession: IAuthorizer) { self.authSession = authSession } @@ -37,7 +37,7 @@ open class APIMethods { return try! encoder.encode(value) } - public func ok(_ response: SDKResponse) throws -> TSuccess { + open func ok(_ response: SDKResponse) throws -> TSuccess { switch response { case .success(let response): return response @@ -53,7 +53,7 @@ open class APIMethods { } } - public func authRequest( + open func authRequest( _ method: HttpMethod, _ path: String, _ queryParams: Values?, @@ -79,7 +79,7 @@ open class APIMethods { } /** Make a GET request */ - func get( + open func get( _ path: String, _ queryParams: Values?, _ body: Any?, @@ -95,7 +95,7 @@ open class APIMethods { } /** Make a HEAD request */ - func head( + open func head( _ path: String, _ queryParams: Values?, _ body: Any?, @@ -111,7 +111,7 @@ open class APIMethods { } /** Make a DELETE request */ - func delete( + open func delete( _ path: String, _ queryParams: Values?, _ body: Any?, @@ -127,7 +127,7 @@ open class APIMethods { } /** Make a POST request */ - func post( + open func post( _ path: String, _ queryParams: Values?, _ body: Any?, @@ -143,7 +143,7 @@ open class APIMethods { } /** Make a PUT request */ - func put( + open func put( _ path: String, _ queryParams: Values?, _ body: Any?, @@ -159,7 +159,7 @@ open class APIMethods { } /** Make a PATCH request */ - func patch( + open func patch( _ path: String, _ queryParams: Values?, _ body: Any?, diff --git a/swift/looker/rtl/apiSettings.swift b/swift/looker/rtl/apiSettings.swift index a4a9fcd6f..a3b8794ea 100644 --- a/swift/looker/rtl/apiSettings.swift +++ b/swift/looker/rtl/apiSettings.swift @@ -100,9 +100,9 @@ public struct ApiSettings: IApiSettings { public var headers: Headers? public var encoding: String? - init() { } + public init() { } - init(_ settings: IApiSettings) throws { + public init(_ settings: IApiSettings) throws { let defaults = DefaultSettings() // coerce types to declared types since some paths could have non-conforming settings values self.base_url = unquote(settings.base_url) ?? defaults.base_url diff --git a/swift/looker/rtl/authSession.swift b/swift/looker/rtl/authSession.swift index 76b0ba431..bcefc75f0 100644 --- a/swift/looker/rtl/authSession.swift +++ b/swift/looker/rtl/authSession.swift @@ -49,7 +49,7 @@ open class AuthSession: IAuthSession { } } - init(_ settings: IApiSettings, _ transport: ITransport? = nil) { + public init(_ settings: IApiSettings, _ transport: ITransport? = nil) { self.settings = settings self.transport = transport ?? BaseTransport(settings) self.apiPath = "/api/\(settings.api_version!)" diff --git a/swift/looker/rtl/authToken.swift b/swift/looker/rtl/authToken.swift index d7f943614..cf61aa7c4 100644 --- a/swift/looker/rtl/authToken.swift +++ b/swift/looker/rtl/authToken.swift @@ -47,9 +47,9 @@ public struct AuthToken: AccessTokenProtocol, Codable { private var expiresAt: Date? - init() { } + public init() { } - init(_ token: AccessToken) { + public init(_ token: AccessToken) { self = self.setToken(token) } diff --git a/swift/looker/rtl/baseTransport.swift b/swift/looker/rtl/baseTransport.swift index ea9e32490..8df25700b 100644 --- a/swift/looker/rtl/baseTransport.swift +++ b/swift/looker/rtl/baseTransport.swift @@ -24,11 +24,11 @@ import Foundation -struct RequestResponse { +public struct RequestResponse { var data: Data? var response: URLResponse? var error: SDKError? - init(_ data: Data?, _ response: URLResponse?, _ error: SDKError?) { + public init(_ data: Data?, _ response: URLResponse?, _ error: SDKError?) { self.data = data self.response = response self.error = error @@ -38,18 +38,18 @@ struct RequestResponse { // some good tips here https://www.swiftbysundell.com/articles/constructing-urls-in-swift/ @available(OSX 10.12, *) -class BaseTransport : ITransport { +open class BaseTransport : ITransport { public static var debugging = false let session = URLSession.shared // TODO Should this be something else like `configuration: .default`? or ephemeral? var apiPath = "" var options: ITransportSettings - init(_ options: ITransportSettings) { + public init(_ options: ITransportSettings) { self.options = options self.apiPath = "\(options.base_url!)/api/\(options.api_version!)" } - func request ( + open func request ( _ method: HttpMethod, _ path: String, _ queryParams: Values?, @@ -71,7 +71,7 @@ class BaseTransport : ITransport { return result } - func plainRequest( + open func plainRequest( _ method: HttpMethod, _ path: String, _ queryParams: Values?, @@ -173,7 +173,7 @@ class BaseTransport : ITransport { } @available(OSX 10.12, *) -func processResponse (_ response: RequestResponse) -> SDKResponse { +public func processResponse (_ response: RequestResponse) -> SDKResponse { if let error = response.error { return SDKResponse.error((error as? TError)!) } diff --git a/swift/looker/rtl/oauthSession.swift b/swift/looker/rtl/oauthSession.swift index e26cbd2f2..a7f763b69 100644 --- a/swift/looker/rtl/oauthSession.swift +++ b/swift/looker/rtl/oauthSession.swift @@ -25,7 +25,7 @@ import Foundation import CryptoKit -// from https://stackoverflow.com/a/57255549/74137 +// partly from https://stackoverflow.com/a/57255549/74137 @available(OSX 10.15, *) extension Digest { var bytes: [UInt8] { Array(makeIterator()) } @@ -33,9 +33,12 @@ extension Digest { var hexStr: String { bytes.map { String(format: "%02x", $0) }.joined() } + var base64Url: String { + data.base64Url() + } } -// from https://stackoverflow.com/a/40089462/74137 +// partly from https://stackoverflow.com/a/40089462/74137 extension Data { struct HexEncodingOptions: OptionSet { let rawValue: Int @@ -52,17 +55,24 @@ extension Data { } return String(utf16CodeUnits: chars, count: chars.count) } + + func base64Url() -> String { + return base64EncodedString() + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "=", with: "") + } } @available(OSX 10.15, *) -class OAuthSession: AuthSession { +open class OAuthSession: AuthSession { var code_verifier: String = "" - override init(_ settings: IApiSettings, _ transport: ITransport? = nil) { + public override init(_ settings: IApiSettings, _ transport: ITransport? = nil) { super.init(settings, transport) } - func requestToken(_ body: Values) throws -> AuthToken { + open func requestToken(_ body: Values) throws -> AuthToken { let response : SDKResponse = self.transport.request( HttpMethod.POST, "/api/token", @@ -81,7 +91,7 @@ class OAuthSession: AuthSession { } } - override func getToken() throws -> AuthToken { + open override func getToken() throws -> AuthToken { if (!self.isAuthenticated()) { if (!self.activeToken.refresh_token.isEmpty) { let config = self.settings.readConfig(nil) @@ -100,8 +110,8 @@ class OAuthSession: AuthSession { /* Generate an OAuth2 authCode request URL */ - func createAuthCodeRequestUrl(scope: String, state: String) throws -> String { - self.code_verifier = try! self.secureRandom(32).hexStr() + public func createAuthCodeRequestUrl(scope: String, state: String) throws -> String { + self.code_verifier = try! self.secureRandom(32).base64Url() let code_challenge = self.sha256Hash(self.code_verifier) let config = self.settings.readConfig(nil) let looker_url = config["looker_url"]! @@ -129,7 +139,7 @@ class OAuthSession: AuthSession { ] } - func redeemAuthCode(_ authCode: String, _ code_verifier: String? = nil) throws -> AuthToken { + public func redeemAuthCode(_ authCode: String, _ code_verifier: String? = nil) throws -> AuthToken { return try self.requestToken(redeemAuthCodeBody(authCode, code_verifier)) } @@ -146,7 +156,7 @@ class OAuthSession: AuthSession { } public func sha256Hash(_ data: Data) -> String { - return SHA256.hash(data: data).hexStr + return SHA256.hash(data: data).base64Url } public func sha256Hash(_ value: String) -> String { diff --git a/swift/looker/rtl/transport.swift b/swift/looker/rtl/transport.swift index a985d1271..b858ec1f4 100644 --- a/swift/looker/rtl/transport.swift +++ b/swift/looker/rtl/transport.swift @@ -125,8 +125,8 @@ public struct SDKError: ISDKError, Codable { private var suggestion: String? private var help: String? - init() { } - init(_ message: String, code: Int = 0, documentation_url: String? = "", reason: String? = "", suggestion: String? = "", help: String? = "") { + public init() { } + public init(_ message: String, code: Int = 0, documentation_url: String? = "", reason: String? = "", suggestion: String? = "", help: String? = "") { self.code = code self.message = message self.reason = reason