Skip to content

Commit

Permalink
Base64 OAuth for Python, Kotlin and Swift SDKs (#296)
Browse files Browse the repository at this point in the history
* Use base64 for Python, Kotlin, and Swift SDK code verification

* added more open/public for SDK classes/structs
  • Loading branch information
jkaster authored Aug 19, 2020
1 parent 96e5aff commit 637a0d6
Show file tree
Hide file tree
Showing 13 changed files with 59 additions and 120 deletions.
81 changes: 7 additions & 74 deletions kotlin/src/main/com/looker/rtl/OAuthSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand All @@ -160,14 +93,13 @@ class OAuthSession(override val apiSettings: ConfigurationProvider, override val
fun redeemAuthCodeBody(authCode: String, codeVerifier: String? = null): Map<String, String> {
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 {
Expand All @@ -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 {
Expand Down
8 changes: 2 additions & 6 deletions kotlin/src/test/TestAuthSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion python/looker_sdk/rtl/auth_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion swift/looker/Tests/lookerTests/authSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion swift/looker/Tests/lookerTests/methodsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class methodsTests: XCTestCase {
XCTAssertNotNil(me)
_ = sdk.authSession.logout()
}

func testUserSearch() {
let list = try? sdk.ok(sdk.search_users(
first_name:"%",
Expand Down
8 changes: 4 additions & 4 deletions swift/looker/rtl/apiConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?
Expand Down
18 changes: 9 additions & 9 deletions swift/looker/rtl/apiMethods.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ open class APIMethods {
public var authSession: IAuthorizer
public var encoder = JSONEncoder()

init(_ authSession: IAuthorizer) {
public init(_ authSession: IAuthorizer) {
self.authSession = authSession
}

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

public func ok<TSuccess, TError>(_ response: SDKResponse<TSuccess, TError>) throws -> TSuccess {
open func ok<TSuccess, TError>(_ response: SDKResponse<TSuccess, TError>) throws -> TSuccess {
switch response {
case .success(let response):
return response
Expand All @@ -53,7 +53,7 @@ open class APIMethods {
}
}

public func authRequest<TSuccess: Codable, TError: Codable>(
open func authRequest<TSuccess: Codable, TError: Codable>(
_ method: HttpMethod,
_ path: String,
_ queryParams: Values?,
Expand All @@ -79,7 +79,7 @@ open class APIMethods {
}

/** Make a GET request */
func get<TSuccess: Codable, TError: Codable>(
open func get<TSuccess: Codable, TError: Codable>(
_ path: String,
_ queryParams: Values?,
_ body: Any?,
Expand All @@ -95,7 +95,7 @@ open class APIMethods {
}

/** Make a HEAD request */
func head<TSuccess: Codable, TError: Codable>(
open func head<TSuccess: Codable, TError: Codable>(
_ path: String,
_ queryParams: Values?,
_ body: Any?,
Expand All @@ -111,7 +111,7 @@ open class APIMethods {
}

/** Make a DELETE request */
func delete<TSuccess: Codable, TError: Codable>(
open func delete<TSuccess: Codable, TError: Codable>(
_ path: String,
_ queryParams: Values?,
_ body: Any?,
Expand All @@ -127,7 +127,7 @@ open class APIMethods {
}

/** Make a POST request */
func post<TSuccess: Codable, TError: Codable>(
open func post<TSuccess: Codable, TError: Codable>(
_ path: String,
_ queryParams: Values?,
_ body: Any?,
Expand All @@ -143,7 +143,7 @@ open class APIMethods {
}

/** Make a PUT request */
func put<TSuccess: Codable, TError: Codable>(
open func put<TSuccess: Codable, TError: Codable>(
_ path: String,
_ queryParams: Values?,
_ body: Any?,
Expand All @@ -159,7 +159,7 @@ open class APIMethods {
}

/** Make a PATCH request */
func patch<TSuccess: Codable, TError: Codable>(
open func patch<TSuccess: Codable, TError: Codable>(
_ path: String,
_ queryParams: Values?,
_ body: Any?,
Expand Down
4 changes: 2 additions & 2 deletions swift/looker/rtl/apiSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion swift/looker/rtl/authSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!)"
Expand Down
4 changes: 2 additions & 2 deletions swift/looker/rtl/authToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
14 changes: 7 additions & 7 deletions swift/looker/rtl/baseTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<TSuccess: Codable, TError: Codable> (
open func request<TSuccess: Codable, TError: Codable> (
_ method: HttpMethod,
_ path: String,
_ queryParams: Values?,
Expand All @@ -71,7 +71,7 @@ class BaseTransport : ITransport {
return result
}

func plainRequest(
open func plainRequest(
_ method: HttpMethod,
_ path: String,
_ queryParams: Values?,
Expand Down Expand Up @@ -173,7 +173,7 @@ class BaseTransport : ITransport {
}

@available(OSX 10.12, *)
func processResponse<TSuccess: Codable, TError: Codable> (_ response: RequestResponse) -> SDKResponse<TSuccess,TError> {
public func processResponse<TSuccess: Codable, TError: Codable> (_ response: RequestResponse) -> SDKResponse<TSuccess,TError> {
if let error = response.error {
return SDKResponse.error((error as? TError)!)
}
Expand Down
Loading

0 comments on commit 637a0d6

Please sign in to comment.