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

BitBucket refresh token flow #222

Merged
merged 5 commits into from
Jan 28, 2016
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
3 changes: 3 additions & 0 deletions BuildaGitServer/Base/BaseTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import ReactiveCocoa

public protocol BuildStatusCreator {
func createStatusFromState(state: BuildState, description: String?, targetUrl: String?) -> StatusType
Expand All @@ -22,6 +23,8 @@ public protocol SourceServerType: BuildStatusCreator {
func postStatusOfCommit(commit: String, status: StatusType, repo: String, completion: (status: StatusType?, error: ErrorType?) -> ())
func postCommentOnIssue(comment: String, issueNumber: Int, repo: String, completion: (comment: CommentType?, error: ErrorType?) -> ())
func getCommentsOfIssue(issueNumber: Int, repo: String, completion: (comments: [CommentType]?, error: ErrorType?) -> ())

func authChangedSignal() -> Signal<ProjectAuthenticator?, NoError>
}

public class SourceServerFactory {
Expand Down
71 changes: 54 additions & 17 deletions BuildaGitServer/BitBucket/BitBucketEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import BuildaUtils
import ReactiveCocoa

class BitBucketEndpoints {

Expand All @@ -19,11 +20,11 @@ class BitBucketEndpoints {
}

private let baseURL: String
private let auth: ProjectAuthenticator?
internal let auth = MutableProperty<ProjectAuthenticator?>(nil)

init(baseURL: String, auth: ProjectAuthenticator?) {
self.baseURL = baseURL
self.auth = auth
self.auth.value = auth
}

private func endpointURL(endpoint: Endpoint, params: [String: String]? = nil) -> String {
Expand Down Expand Up @@ -75,6 +76,43 @@ class BitBucketEndpoints {

}

func setAuthOnRequest(request: NSMutableURLRequest) {

guard let auth = self.auth.value else { return }

switch auth.type {
case .OAuthToken:
let tokens = auth.secret.componentsSeparatedByString(":")
//first is refresh token, second access token
request.setValue("Bearer \(tokens[1])", forHTTPHeaderField:"Authorization")
default:
fatalError("This kind of authentication is not supported for BitBucket")
}
}

func createRefreshTokenRequest() -> NSMutableURLRequest {

guard let auth = self.auth.value else { fatalError("No auth") }
let refreshUrl = auth.service.accessTokenUrl()
let refreshToken = auth.secret.componentsSeparatedByString(":")[0]
let body = [
("grant_type", "refresh_token"),
("refresh_token", refreshToken)
].map { "\($0.0)=\($0.1)" }.joinWithSeparator("&")

let request = NSMutableURLRequest(URL: NSURL(string: refreshUrl)!)

let service = auth.service
let servicePublicKey = service.serviceKey()
let servicePrivateKey = service.serviceSecret()
let credentials = "\(servicePublicKey):\(servicePrivateKey)".base64String()
request.setValue("Basic \(credentials)", forHTTPHeaderField:"Authorization")

request.HTTPMethod = "POST"
self.setStringBody(request, body: body)
return request
}

func createRequest(method: HTTP.Method, endpoint: Endpoint, params: [String : String]? = nil, query: [String : String]? = nil, body: NSDictionary? = nil) throws -> NSMutableURLRequest {

let endpointURL = self.endpointURL(endpoint, params: params)
Expand All @@ -86,25 +124,24 @@ class BitBucketEndpoints {
let request = NSMutableURLRequest(URL: url)

request.HTTPMethod = method.rawValue
if let auth = self.auth {

switch auth.type {
case .OAuthToken:
let tokens = auth.secret.componentsSeparatedByString(":")
//first is refresh token, second access token
request.setValue("Bearer \(tokens[1])", forHTTPHeaderField:"Authorization")
default:
fatalError("This kind of authentication is not supported for BitBucket")
}
}
self.setAuthOnRequest(request)

if let body = body {

let data = try NSJSONSerialization.dataWithJSONObject(body, options: NSJSONWritingOptions())
request.HTTPBody = data
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
try self.setJSONBody(request, body: body)
}

return request
}

func setStringBody(request: NSMutableURLRequest, body: String) {
let data = body.dataUsingEncoding(NSUTF8StringEncoding)
request.HTTPBody = data
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
}

func setJSONBody(request: NSMutableURLRequest, body: NSDictionary) throws {
let data = try NSJSONSerialization.dataWithJSONObject(body, options: NSJSONWritingOptions())
request.HTTPBody = data
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
}
}
104 changes: 82 additions & 22 deletions BuildaGitServer/BitBucket/BitBucketServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
//

import Foundation

import BuildaUtils
import ReactiveCocoa

class BitBucketServer : GitServer {

Expand All @@ -20,6 +20,12 @@ class BitBucketServer : GitServer {
self.endpoints = endpoints
super.init(service: .GitHub, http: http)
}

override func authChangedSignal() -> Signal<ProjectAuthenticator?, NoError> {
var res: Signal<ProjectAuthenticator?, NoError>?
self.endpoints.auth.producer.startWithSignal { res = $0.0 }
return res!
}
}

extension BitBucketServer: SourceServerType {
Expand Down Expand Up @@ -191,36 +197,31 @@ extension BitBucketServer: SourceServerType {

extension BitBucketServer {

private func _sendRequest(request: NSMutableURLRequest, completion: HTTP.Completion) {
private func _sendRequest(request: NSMutableURLRequest, isRetry: Bool = false, completion: HTTP.Completion) {

// let cachedInfo = self.cache.getCachedInfoForRequest(request)
// if let etag = cachedInfo.etag {
// request.setValue(etag, forHTTPHeaderField: "If-None-Match")
// }

self.http.sendRequest(request, completion: { (response, body, error) -> () in
self.http.sendRequest(request) { (response, body, error) -> () in

if let error = error {
completion(response: response, body: body, error: error)
return
}

if response == nil {
completion(response: nil, body: body, error: Error.withInfo("Nil response"))
return
}

// if let response = response {
// let headers = response.allHeaderFields
//
//
// if
// let resetTime = (headers["X-RateLimit-Reset"] as? NSString)?.doubleValue,
// let limit = (headers["X-RateLimit-Limit"] as? NSString)?.integerValue,
// let remaining = (headers["X-RateLimit-Remaining"] as? NSString)?.integerValue {
//
//
// let rateLimitInfo = GitHubRateLimit(resetTime: resetTime, limit: limit, remaining: remaining)
// self.latestRateLimitInfo = rateLimitInfo
//
//
// } else {
// Log.error("No X-RateLimit info provided by GitHub in headers: \(headers), we're unable to detect the remaining number of allowed requests. GitHub might fail to return data any time now :(")
// }
Expand All @@ -229,16 +230,19 @@ extension BitBucketServer {
//error out on special HTTP status codes
let statusCode = response!.statusCode
switch statusCode {
// case 200...299: //good response, cache the returned data
// let responseInfo = ResponseInfo(response: response!, body: body)
// cachedInfo.update(responseInfo)
// case 304: //not modified, return the cached response
// let responseInfo = cachedInfo.responseInfo!
// completion(response: responseInfo.response, body: responseInfo.body, error: nil)
// return
case 401: //TODO: handle unauthorized, use refresh token to get a new
//access token
break
// case 200...299: //good response, cache the returned data
// let responseInfo = ResponseInfo(response: response!, body: body)
// cachedInfo.update(responseInfo)
// case 304: //not modified, return the cached response
// let responseInfo = cachedInfo.responseInfo!
// completion(response: responseInfo.response, body: responseInfo.body, error: nil)
// return
case 401: //unauthorized, use refresh token to get a new access token
//only try to refresh token once
if !isRetry {
self._handle401(request, completion: completion)
}
return
case 400, 402 ... 500:

let message = ((body as? NSDictionary)?["error"] as? NSDictionary)?["message"] as? String ?? (body as? String ?? "Unknown error")
Expand All @@ -250,7 +254,63 @@ extension BitBucketServer {
}

completion(response: response, body: body, error: error)
})
}
}

private func _handle401(request: NSMutableURLRequest, completion: HTTP.Completion) {

//we need to use the refresh token to request a new access token
//then we need to notify that we updated the secret, so that it can
//be saved by buildasaur
//then we need to set the new access token to this waiting request and
//run it again. if that fails too, we fail for real.

Log.verbose("Got 401, starting a BitBucket refresh token flow...")

//get a new access token
self._refreshAccessToken(request) { error in

if let error = error {
Log.verbose("Failed to get a new access token")
completion(response: nil, body: nil, error: error)
return
}

//we have a new access token, force set the new cred on the original
//request
self.endpoints.setAuthOnRequest(request)

Log.verbose("Successfully refreshed a BitBucket access token")

//retrying the original request
self._sendRequest(request, isRetry: true, completion: completion)
}
}

private func _refreshAccessToken(request: NSMutableURLRequest, completion: (NSError?) -> ()) {

let refreshRequest = self.endpoints.createRefreshTokenRequest()
self.http.sendRequest(refreshRequest) { (response, body, error) -> () in

if let error = error {
completion(error)
return
}

guard response?.statusCode == 200 else {
completion(Error.withInfo("Wrong status code returned, refreshing access token failed"))
return
}

let payload = body as! NSDictionary
let accessToken = payload.stringForKey("access_token")
let refreshToken = payload.stringForKey("refresh_token")
let secret = [refreshToken, accessToken].joinWithSeparator(":")

let newAuth = ProjectAuthenticator(service: .BitBucket, username: "GIT", type: .OAuthToken, secret: secret)
self.endpoints.auth.value = newAuth
completion(nil)
}
}

private func _sendRequestWithMethod(method: HTTP.Method, endpoint: BitBucketEndpoints.Endpoint, params: [String: String]?, query: [String: String]?, body: NSDictionary?, completion: HTTP.Completion) {
Expand Down
19 changes: 19 additions & 0 deletions BuildaGitServer/Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Extensions.swift
// Buildasaur
//
// Created by Honza Dvorsky on 1/28/16.
// Copyright © 2016 Honza Dvorsky. All rights reserved.
//

import Foundation

extension String {

public func base64String() -> String {
return self
.dataUsingEncoding(NSUTF8StringEncoding)!
.base64EncodedStringWithOptions(NSDataBase64EncodingOptions())
}
}

1 change: 1 addition & 0 deletions BuildaGitServer/GitHub/GitHubServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import BuildaUtils
import ReactiveCocoa

class GitHubServer : GitServer {

Expand Down
35 changes: 35 additions & 0 deletions BuildaGitServer/GitServerPublic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import Foundation
import BuildaUtils
import Keys
import ReactiveCocoa

public enum GitService: String {
case GitHub = "github"
Expand All @@ -34,11 +36,44 @@ public enum GitService: String {
case .BitBucket: return "bitbucket.org"
}
}

public func authorizeUrl() -> String {
switch self {
case .GitHub: return "https://github.com/login/oauth/authorize"
case .BitBucket: return "https://bitbucket.org/site/oauth2/authorize"
}
}

public func accessTokenUrl() -> String {
switch self {
case .GitHub: return "https://github.com/login/oauth/access_token"
case .BitBucket: return "https://bitbucket.org/site/oauth2/access_token"
}
}

public func serviceKey() -> String {
switch self {
case .GitHub: return BuildasaurKeys().gitHubAPIClientId()
case .BitBucket: return BuildasaurKeys().bitBucketAPIClientId()
}
}

public func serviceSecret() -> String {
switch self {
case .GitHub: return BuildasaurKeys().gitHubAPIClientSecret()
case .BitBucket: return BuildasaurKeys().bitBucketAPIClientSecret()
}
}
}

public class GitServer : HTTPServer {

let service: GitService

public func authChangedSignal() -> Signal<ProjectAuthenticator?, NoError> {
return Signal.never
}

init(service: GitService, http: HTTP? = nil) {
self.service = service
super.init(http: http)
Expand Down
Loading