Skip to content

Commit

Permalink
Merge pull request #222 from czechboy0/hd/bitbucket_refresh_token
Browse files Browse the repository at this point in the history
BitBucket refresh token flow
  • Loading branch information
czechboy0 committed Jan 28, 2016
2 parents 6056488 + c084c88 commit 0fe5c99
Show file tree
Hide file tree
Showing 41 changed files with 304 additions and 405 deletions.
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

0 comments on commit 0fe5c99

Please sign in to comment.