Skip to content

Commit 637a0d6

Browse files
authored
Base64 OAuth for Python, Kotlin and Swift SDKs (#296)
* Use base64 for Python, Kotlin, and Swift SDK code verification * added more open/public for SDK classes/structs
1 parent 96e5aff commit 637a0d6

13 files changed

+59
-120
lines changed

kotlin/src/main/com/looker/rtl/OAuthSession.kt

+7-74
Original file line numberDiff line numberDiff line change
@@ -27,77 +27,10 @@ package com.looker.rtl
2727
import com.looker.sdk.AccessToken
2828
import java.security.MessageDigest
2929
import java.security.SecureRandom
30-
import kotlin.experimental.and
30+
import java.util.Base64
3131

32-
// https://stackoverflow.com/a/52225984/74137
33-
// TODO performance comparison of these two methods
34-
@ExperimentalUnsignedTypes
35-
fun ByteArray.toHexStr() = asUByteArray().joinToString("") { it.toString(16).padStart(2, '0') }
36-
37-
// Adapted from https://www.samclarke.com/kotlin-hash-strings/
38-
39-
fun String.md5(): String {
40-
return hashString(this, "MD5")
41-
}
42-
43-
fun String.sha512(): String {
44-
return hashString(this, "SHA-512")
45-
}
46-
47-
fun String.sha256(): String {
48-
return hashString(this, "SHA-256")
49-
}
50-
51-
fun String.sha1(): String {
52-
return hashString(this, "SHA-1")
53-
}
54-
55-
fun hashString(input: ByteArray, digester: MessageDigest): String {
56-
val HEX_CHARS = "0123456789abcdef"
57-
val bytes = digester
58-
.digest(input)
59-
val result = StringBuilder(bytes.size * 2)
60-
61-
bytes.forEach {
62-
val i = it.toInt()
63-
result.append(HEX_CHARS[i shr 4 and 0x0f])
64-
result.append(HEX_CHARS[i and 0x0f])
65-
}
66-
67-
return result.toString()
68-
}
69-
70-
fun hashString(input: ByteArray, type: String): String {
71-
val digester = MessageDigest.getInstance(type)
72-
return hashString(input, digester)
73-
}
74-
75-
/**
76-
* Supported algorithms on Android:
77-
*
78-
* Algorithm Supported API Levels
79-
* MD5 1+
80-
* SHA-1 1+
81-
* SHA-224 1-8,22+
82-
* SHA-256 1+
83-
* SHA-384 1+
84-
* SHA-512 1+
85-
*/
86-
fun hashString(input: String, type: String): String {
87-
return hashString(input.toByteArray(), type)
88-
}
89-
90-
private val hexArray = "0123456789abcdef".toCharArray()
91-
92-
fun hexStr(bytes: ByteArray): String {
93-
val hexChars = CharArray(bytes.size * 2)
94-
for (j in bytes.indices) {
95-
val v = (bytes[j] and 0xFF.toByte()).toInt()
96-
97-
hexChars[j * 2] = hexArray[v ushr 4]
98-
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
99-
}
100-
return String(hexChars)
32+
fun base64UrlEncode(bytes: ByteArray): String {
33+
return Base64.getUrlEncoder().encodeToString(bytes)
10134
}
10235

10336
@ExperimentalUnsignedTypes
@@ -142,7 +75,7 @@ class OAuthSession(override val apiSettings: ConfigurationProvider, override val
14275
* Generate an OAuth2 authCode request URL
14376
*/
14477
fun createAuthCodeRequestUrl(scope: String, state: String): String {
145-
this.codeVerifier = this.secureRandom(32).toHexStr()
78+
this.codeVerifier = base64UrlEncode(this.secureRandom(32))
14679
val codeChallenge = this.sha256hash(this.codeVerifier)
14780
val config = this.apiSettings.readConfig()
14881
val lookerUrl = config["looker_url"]
@@ -160,14 +93,13 @@ class OAuthSession(override val apiSettings: ConfigurationProvider, override val
16093
fun redeemAuthCodeBody(authCode: String, codeVerifier: String? = null): Map<String, String> {
16194
val verifier = codeVerifier?: this.codeVerifier
16295
val config = this.apiSettings.readConfig()
163-
val map = mapOf(
96+
return mapOf(
16497
"grant_type" to "authorization_code",
16598
"code" to authCode,
16699
"code_verifier" to verifier,
167100
"client_id" to (config["client_id"] ?: error("")),
168101
"redirect_uri" to (config["redirect_uri"] ?: error(""))
169102
)
170-
return map
171103
}
172104

173105
fun redeemAuthCode(authCode: String, codeVerifier: String? = null): AuthToken {
@@ -181,7 +113,8 @@ class OAuthSession(override val apiSettings: ConfigurationProvider, override val
181113
}
182114

183115
fun sha256hash(value: ByteArray): String {
184-
return hashString(value, messageDigest)
116+
val bytes = messageDigest.digest(value)
117+
return base64UrlEncode(bytes)
185118
}
186119

187120
fun sha256hash(value: String): String {

kotlin/src/test/TestAuthSession.kt

+2-6
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,9 @@ class TestAuthSession {
7676
@test
7777
fun testSha256() {
7878
val session = OAuthSession(settings, Transport(testSettings))
79-
val rosettaCode = "Rosetta code"
80-
val rosettaHash = "764faf5c61ac315f1497f9dfa542713965b785e5cc2f707d6468d7d1124cdfcf"
81-
var hash = session.sha256hash(rosettaCode)
82-
assertEquals(rosettaHash, hash, "Rosetta code should match")
8379
val message = "The quick brown fox jumped over the lazy dog."
84-
hash = session.sha256hash(message)
85-
assertEquals("68b1282b91de2c054c36629cb8dd447f12f096d3e3c587978dc2248444633483", hash, "Quick brown fox should match")
80+
val hash = session.sha256hash(message)
81+
assertEquals("aLEoK5HeLAVMNmKcuN1EfxLwltPjxYeXjcIkhERjNIM=", hash, "Quick brown fox should match")
8682
}
8783

8884
@test

python/looker_sdk/rtl/auth_session.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def _ok(self, response: transport.Response) -> str:
223223

224224
class CryptoHash:
225225
def secure_random(self, byte_count: int) -> str:
226-
return secrets.token_hex()
226+
return secrets.token_urlsafe(byte_count)
227227

228228
def sha256_hash(self, message: str) -> str:
229229
value = hashlib.sha256()

swift/looker/Tests/lookerTests/authSessionTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class authSessionTests: XCTestCase {
4141
let session = OAuthSession(settings, xp)
4242
let message = "The quick brown fox jumped over the lazy dog."
4343
let hash = session.sha256Hash(message)
44-
XCTAssertEqual("68b1282b91de2c054c36629cb8dd447f12f096d3e3c587978dc2248444633483", hash)
44+
XCTAssertEqual("aLEoK5HeLAVMNmKcuN1EfxLwltPjxYeXjcIkhERjNIM", hash)
4545
}
4646

4747
func testRedemptionBody() {

swift/looker/Tests/lookerTests/methodsTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class methodsTests: XCTestCase {
9797
XCTAssertNotNil(me)
9898
_ = sdk.authSession.logout()
9999
}
100-
100+
101101
func testUserSearch() {
102102
let list = try? sdk.ok(sdk.search_users(
103103
first_name:"%",

swift/looker/rtl/apiConfig.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public func parseConfig(_ filename : String) -> Config {
7979
return config
8080
}
8181

82-
public class ApiConfig: IApiSettings {
82+
open class ApiConfig: IApiSettings {
8383
public func readConfig(_ section: String? = nil) -> IApiSection {
8484
if (self.fileName == "") {
8585
// No config file to read
@@ -103,16 +103,16 @@ public class ApiConfig: IApiSettings {
103103
private var fileName = ""
104104
private var section = "Looker"
105105

106-
init() {
106+
public init() {
107107
self.assign(DefaultSettings())
108108
}
109109

110-
init(_ settings: IApiSettings) {
110+
public init(_ settings: IApiSettings) {
111111
self.assign(settings)
112112
}
113113

114114
/// Get SDK settings from a configuration file with environment variable overrides
115-
init(_ fileName: String = "", _ section: String = "Looker") throws {
115+
public init(_ fileName: String = "", _ section: String = "Looker") throws {
116116
let fm = FileManager.default
117117
if (fileName == "") {
118118
// Default file name to looker.ini?

swift/looker/rtl/apiMethods.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ open class APIMethods {
2929
public var authSession: IAuthorizer
3030
public var encoder = JSONEncoder()
3131

32-
init(_ authSession: IAuthorizer) {
32+
public init(_ authSession: IAuthorizer) {
3333
self.authSession = authSession
3434
}
3535

3636
open func encode<T>(_ value: T) throws -> Data where T : Encodable {
3737
return try! encoder.encode(value)
3838
}
3939

40-
public func ok<TSuccess, TError>(_ response: SDKResponse<TSuccess, TError>) throws -> TSuccess {
40+
open func ok<TSuccess, TError>(_ response: SDKResponse<TSuccess, TError>) throws -> TSuccess {
4141
switch response {
4242
case .success(let response):
4343
return response
@@ -53,7 +53,7 @@ open class APIMethods {
5353
}
5454
}
5555

56-
public func authRequest<TSuccess: Codable, TError: Codable>(
56+
open func authRequest<TSuccess: Codable, TError: Codable>(
5757
_ method: HttpMethod,
5858
_ path: String,
5959
_ queryParams: Values?,
@@ -79,7 +79,7 @@ open class APIMethods {
7979
}
8080

8181
/** Make a GET request */
82-
func get<TSuccess: Codable, TError: Codable>(
82+
open func get<TSuccess: Codable, TError: Codable>(
8383
_ path: String,
8484
_ queryParams: Values?,
8585
_ body: Any?,
@@ -95,7 +95,7 @@ open class APIMethods {
9595
}
9696

9797
/** Make a HEAD request */
98-
func head<TSuccess: Codable, TError: Codable>(
98+
open func head<TSuccess: Codable, TError: Codable>(
9999
_ path: String,
100100
_ queryParams: Values?,
101101
_ body: Any?,
@@ -111,7 +111,7 @@ open class APIMethods {
111111
}
112112

113113
/** Make a DELETE request */
114-
func delete<TSuccess: Codable, TError: Codable>(
114+
open func delete<TSuccess: Codable, TError: Codable>(
115115
_ path: String,
116116
_ queryParams: Values?,
117117
_ body: Any?,
@@ -127,7 +127,7 @@ open class APIMethods {
127127
}
128128

129129
/** Make a POST request */
130-
func post<TSuccess: Codable, TError: Codable>(
130+
open func post<TSuccess: Codable, TError: Codable>(
131131
_ path: String,
132132
_ queryParams: Values?,
133133
_ body: Any?,
@@ -143,7 +143,7 @@ open class APIMethods {
143143
}
144144

145145
/** Make a PUT request */
146-
func put<TSuccess: Codable, TError: Codable>(
146+
open func put<TSuccess: Codable, TError: Codable>(
147147
_ path: String,
148148
_ queryParams: Values?,
149149
_ body: Any?,
@@ -159,7 +159,7 @@ open class APIMethods {
159159
}
160160

161161
/** Make a PATCH request */
162-
func patch<TSuccess: Codable, TError: Codable>(
162+
open func patch<TSuccess: Codable, TError: Codable>(
163163
_ path: String,
164164
_ queryParams: Values?,
165165
_ body: Any?,

swift/looker/rtl/apiSettings.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ public struct ApiSettings: IApiSettings {
100100
public var headers: Headers?
101101
public var encoding: String?
102102

103-
init() { }
103+
public init() { }
104104

105-
init(_ settings: IApiSettings) throws {
105+
public init(_ settings: IApiSettings) throws {
106106
let defaults = DefaultSettings()
107107
// coerce types to declared types since some paths could have non-conforming settings values
108108
self.base_url = unquote(settings.base_url) ?? defaults.base_url

swift/looker/rtl/authSession.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ open class AuthSession: IAuthSession {
4949
}
5050
}
5151

52-
init(_ settings: IApiSettings, _ transport: ITransport? = nil) {
52+
public init(_ settings: IApiSettings, _ transport: ITransport? = nil) {
5353
self.settings = settings
5454
self.transport = transport ?? BaseTransport(settings)
5555
self.apiPath = "/api/\(settings.api_version!)"

swift/looker/rtl/authToken.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ public struct AuthToken: AccessTokenProtocol, Codable {
4747

4848
private var expiresAt: Date?
4949

50-
init() { }
50+
public init() { }
5151

52-
init(_ token: AccessToken) {
52+
public init(_ token: AccessToken) {
5353
self = self.setToken(token)
5454
}
5555

swift/looker/rtl/baseTransport.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424

2525
import Foundation
2626

27-
struct RequestResponse {
27+
public struct RequestResponse {
2828
var data: Data?
2929
var response: URLResponse?
3030
var error: SDKError?
31-
init(_ data: Data?, _ response: URLResponse?, _ error: SDKError?) {
31+
public init(_ data: Data?, _ response: URLResponse?, _ error: SDKError?) {
3232
self.data = data
3333
self.response = response
3434
self.error = error
@@ -38,18 +38,18 @@ struct RequestResponse {
3838

3939
// some good tips here https://www.swiftbysundell.com/articles/constructing-urls-in-swift/
4040
@available(OSX 10.12, *)
41-
class BaseTransport : ITransport {
41+
open class BaseTransport : ITransport {
4242
public static var debugging = false
4343
let session = URLSession.shared // TODO Should this be something else like `configuration: .default`? or ephemeral?
4444
var apiPath = ""
4545
var options: ITransportSettings
4646

47-
init(_ options: ITransportSettings) {
47+
public init(_ options: ITransportSettings) {
4848
self.options = options
4949
self.apiPath = "\(options.base_url!)/api/\(options.api_version!)"
5050
}
5151

52-
func request<TSuccess: Codable, TError: Codable> (
52+
open func request<TSuccess: Codable, TError: Codable> (
5353
_ method: HttpMethod,
5454
_ path: String,
5555
_ queryParams: Values?,
@@ -71,7 +71,7 @@ class BaseTransport : ITransport {
7171
return result
7272
}
7373

74-
func plainRequest(
74+
open func plainRequest(
7575
_ method: HttpMethod,
7676
_ path: String,
7777
_ queryParams: Values?,
@@ -173,7 +173,7 @@ class BaseTransport : ITransport {
173173
}
174174

175175
@available(OSX 10.12, *)
176-
func processResponse<TSuccess: Codable, TError: Codable> (_ response: RequestResponse) -> SDKResponse<TSuccess,TError> {
176+
public func processResponse<TSuccess: Codable, TError: Codable> (_ response: RequestResponse) -> SDKResponse<TSuccess,TError> {
177177
if let error = response.error {
178178
return SDKResponse.error((error as? TError)!)
179179
}

0 commit comments

Comments
 (0)