Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base64 OAuth for Kotlin and Swift SDKs #296

Merged
merged 3 commits into from
Aug 19, 2020
Merged
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
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