From 26fcfdc66bb82929352c443e4214fc3a4a6e45e6 Mon Sep 17 00:00:00 2001 From: Youming Lin Date: Wed, 31 Aug 2016 11:12:47 -0500 Subject: [PATCH] Implementation for web-based GitHub login (#2) * Implementation for web-based GitHub login * Update Makefile to point to Package-Builder * Update .travis.yml --- .gitignore | 6 +- .gitmodules | 3 + .swift-version | 1 + .travis.yml | 28 ++++ CONTRIBUTING.md | 33 +++++ Makefile | 22 +++ Package-Builder | 1 + Package.swift | 24 +++ README.md | 59 ++++++++ .../CredentialsGitHub/CredentialsGitHub.swift | 137 ++++++++++++++++++ 10 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 .swift-version create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 160000 Package-Builder create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/CredentialsGitHub/CredentialsGitHub.swift diff --git a/.gitignore b/.gitignore index 2c22487..61ad847 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ +.DS_Store + # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +Kitura-CredentialsGitHub.xcodeproj/ ## Build generated +.build/ build/ DerivedData/ @@ -34,8 +38,8 @@ playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ .build/ +Packages/ # CocoaPods # diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..aab1be1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Package-Builder"] + path = Package-Builder + url = https://github.com/IBM-Swift/Package-Builder.git diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..22ea19a --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +DEVELOPMENT-SNAPSHOT-2016-08-23-a diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e8330d6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +# Travis CI build file for Kitura-redis. +# Kitura runs on OS X and Linux (Ubuntu v15.10). + +# whitelist (branches that should be built) +branches: + only: + - master + - develop + - /^issue.*$/ + +notifications: + slack: + secure: "spl/MlD2mVGlto7/zMYQrswJDOM/2T/IcJ7AgrJCZ1spPZeM7HqKzgIwjlfK2Um65gzYITECTW6DEMI4pgporqHkKpCv56VCl2qoiwy3jFj2UMBFF7Bzga8hEZSygtREVz359rBZ9hfZb1+UKS/5z95Ewdm4GOIfX8GlqvWRISZqq9LpJVjw3qXg3JbRJMJJPJBCjHyEGEY4WoIYTvOifAphGNXTu9ELQeYh+Vfoq1S5lFwjwlVc7H2QYRvtQc5sNQVylq/sGbBr9AGCXUbnexmz+7VNI7+XiRcDO2EoXpHAuCZAkI+lwlXwqv2JZuHFv/1VKIRArL46pLxeQyf6RN8wWvKMfsBx/fR5KOX2InUeYeBl6yCN9A7+gMjBYamg8UuRkM7JgzChVoZz9uQrQZaQOUwo3hWbNwJK20kpAO12LKN6jOgkp+EsoRIO/j9eLxbTJBPsGIJ5YDGRUn8bMY6yg1OEiS17bmjPMIUikk8o8GXGntfHDMMTzyeYPdSmDjuaBkDs+/b5jIxERw5qVI4f3lyaFxe+S24aA6wT3sQjmBCUuo15uoSpiwsarp2k66pnNgA63oeMn+JmdInRTEgC7ZwDCMGdLZpIFt9fhHlGGvbgrhDPxmaNKe1GAWICjisQiO4GTw6iNgy4VAqGBkuG6uzNYBiLd0QfW0LlY6s=" + +matrix: + include: + - os: linux + dist: trusty + sudo: required + - os: osx + osx_image: xcode8 + sudo: required + +before_install: + - git submodule update --init --remote --merge --recursive + +script: + - ./Package-Builder/build-package.sh $TRAVIS_BRANCH $TRAVIS_BUILD_DIR diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4858d20 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing to Kitura + +We welcome contributions, but request you follow these guidelines. + + - [Raising issues](#raising-issues) + - [Contributor License Agreement](#contributor-license-agreement) + - [Coding Standards](#coding-standards) + + +## Raising issues + +Please raise any bug reports on the issue tracker. Be sure to +search the list to see if your issue has already been raised. + +A good bug report is one that make it easy for us to understand what you were +trying to do and what went wrong. Provide as much context as possible so we can try to recreate the issue. + +### Contributor License Agreement + +In order for us to accept pull-requests, the contributor must first complete +a Contributor License Agreement (CLA). Please see our [CLA repo](http://github.com/IBM-Swift/CLA) for more information. + +This clarifies the intellectual property license granted with any contribution. It is for your protection as a +Contributor as well as the protection of IBM and its customers; it does not +change your rights to use your own Contributions for any other purpose. + +### Coding standards + +Please ensure you follow the coding standards used throughout the existing +code base. Some basic rules include: + + - all files must have the Apache license in the header. + - all PRs must have passing builds for all operating systems. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fbb74b7 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# Copyright IBM Corporation 2016 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Makefile +export KITURA_CI_BUILD_SCRIPTS_DIR=Package-Builder/build + +-include Package-Builder/build/Makefile + +Package-Builder/build/Makefile: + @echo --- Fetching Package-Builder submodule + git submodule update --init --remote --merge --recursive diff --git a/Package-Builder b/Package-Builder new file mode 160000 index 0000000..eff80f5 --- /dev/null +++ b/Package-Builder @@ -0,0 +1 @@ +Subproject commit eff80f58b8c3fce2da57e253a068bea749ef7a6a diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..972c3c8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,24 @@ +/** + * Copyright IBM Corporation 2016 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import PackageDescription + +let package = Package( + name: "Kitura-CredentialsGitHub", + dependencies: [ + .Package(url: "https://github.com/IBM-Swift/Kitura-Credentials.git", majorVersion: 0, minor: 28), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..64d5cd7 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Kitura-CredentialsGitHub +Plugin for the Credentials framework that authenticate using GitHub + +![Mac OS X](https://img.shields.io/badge/os-Mac%20OS%20X-green.svg?style=flat) +![Linux](https://img.shields.io/badge/os-linux-green.svg?style=flat) +![Apache 2](https://img.shields.io/badge/license-Apache2-blue.svg?style=flat) + +## Summary +Plugin for [Kitura-Credentials](https://github.com/IBM-Swift/Kitura-Credentials) framework that authenticates using the [GitHub web login with OAuth](https://developer.github.com/v3/oauth/#web-application-flow). + +## Table of Contents +* [Swift version](#swift-version) +* [Example of GitHub web login](#example-of-github-web-login) +* [License](#license) + +## Swift version +The latest version of Kitura-CredentialsGitHub works with the DEVELOPMENT-SNAPSHOT-2016-08-23-a version of the Swift binaries. You can download this version of the Swift binaries by following this [link](https://swift.org/download/). Compatibility with other Swift versions is not guaranteed. + +## Example of GitHub web login +First create an instance of `CredentialsGitHub` plugin and register it with `Credentials` framework: +```swift +import Credentials +import CredentialsGitHub + +let credentials = Credentials() +let gitCredentials = CredentialsGitHub(clientId: gitClientId, clientSecret: gitClientSecret, callbackUrl: serverUrl + "/login/github/callback", userAgent: "my-kitura-app") +credentials.register(gitCredentials) +``` +**Where:** +- *gitClientId* is the Client ID of your app in your GitHub Developer applications +- *gitClientSecret* is the Client Secret of your app in your GitHub Developer applications +- *userAgent* is an optional argument that passes along a User-Agent of your choice on API calls against GitHub. By default, `Kitura-CredentialsGitHub` is set as the User-Agent. [User-Agent is required when invoking GitHub APIs](https://developer.github.com/v3/#user-agent-required). + +**Note:** The *callbackUrl* parameter above is used to tell the GitHub web login page where the user's browser should be redirected when the login is successful. It should be a URL handled by the server you are writing. +Specify where to redirect non-authenticated requests: +```swift +credentials.options["failureRedirect"] = "/login/github" +``` + +Connect `credentials` middleware to requests to `/private`: + +```swift +router.all("/private", middleware: credentials) +router.get("/private/data", handler: { request, response, next in + ... + next() +}) +``` +And call `authenticate` to login with GitHub and to handle the redirect (callback) from the GitHub login web page after a successful login: + +```swift +router.get("/login/github", +handler: credentials.authenticate(gitCredentials.name)) + +router.get("/login/github/callback", +handler: credentials.authenticate(gitCredentials.name)) +``` +## License +This library is licensed under Apache 2.0. Full license text is available in [LICENSE](LICENSE.txt). diff --git a/Sources/CredentialsGitHub/CredentialsGitHub.swift b/Sources/CredentialsGitHub/CredentialsGitHub.swift new file mode 100644 index 0000000..4f05f9f --- /dev/null +++ b/Sources/CredentialsGitHub/CredentialsGitHub.swift @@ -0,0 +1,137 @@ +/** + * Copyright IBM Corporation 2016 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import Kitura +import KituraNet +import LoggerAPI +import Credentials + +import SwiftyJSON + +import Foundation + +public class CredentialsGitHub : CredentialsPluginProtocol { + + private var clientId : String + + private var clientSecret : String + + public var callbackUrl : String + + /// User-Agent must be set in order to access GitHub API (i.e., to get user profile) + /// https://developer.github.com/v3/#user-agent-required + public private(set) var userAgent: String + + public var name : String { + return "GitHub" + } + + public var redirecting : Bool { + return true + } + + public init (clientId: String, clientSecret : String, callbackUrl : String, userAgent: String?=nil) { + self.clientId = clientId + self.clientSecret = clientSecret + self.callbackUrl = callbackUrl + self.userAgent = userAgent ?? "Kitura-CredentialsGitHub" + } + + public var usersCache : NSCache? + + /// https://developer.github.com/v3/oauth/#web-application-flow + public func authenticate (request: RouterRequest, response: RouterResponse, + options: [String:Any], onSuccess: @escaping (UserProfile) -> Void, + onFailure: @escaping (HTTPStatusCode?, [String:String]?) -> Void, + onPass: @escaping (HTTPStatusCode?, [String:String]?) -> Void, + inProgress: @escaping () -> Void) { + if let code = request.queryParameters["code"] { + // query contains code: exchange code for access token + var requestOptions: [ClientRequest.Options] = [] + requestOptions.append(.schema("https://")) + requestOptions.append(.hostname("github.com")) + requestOptions.append(.method("POST")) + requestOptions.append(.path("/login/oauth/access_token?client_id=\(clientId)&redirect_uri=\(callbackUrl)&client_secret=\(clientSecret)&code=\(code)")) + var headers = [String:String]() + headers["Accept"] = "application/json" + requestOptions.append(.headers(headers)) + + let requestForToken = HTTP.request(requestOptions) { fbResponse in + if let fbResponse = fbResponse, fbResponse.statusCode == .OK { + // get user profile with access token + do { + var body = Data() + try fbResponse.readAllData(into: &body) + var jsonBody = JSON(data: body) + if let token = jsonBody["access_token"].string { + requestOptions = [] + requestOptions.append(.schema("https://")) + requestOptions.append(.hostname("api.github.com")) + requestOptions.append(.method("GET")) + requestOptions.append(.path("/user")) + headers = [String:String]() + headers["Accept"] = "application/json" + headers["User-Agent"] = self.userAgent + headers["Authorization"] = "token \(token)" + requestOptions.append(.headers(headers)) + + let requestForProfile = HTTP.request(requestOptions) { profileResponse in + if let profileResponse = profileResponse, profileResponse.statusCode == .OK { + do { + body = Data() + try profileResponse.readAllData(into: &body) + jsonBody = JSON(data: body) + + if let id = jsonBody["id"].number?.stringValue, + let name = jsonBody["name"].string { + let userProfile = UserProfile(id: id, displayName: name, provider: self.name) + onSuccess(userProfile) + return + } + } + catch { + Log.error("Failed to read \(self.name) response") + } + } + else { + onFailure(nil, nil) + } + } + requestForProfile.end() + } + } + catch { + Log.error("Failed to read \(self.name) response") + } + } + else { + onFailure(nil, nil) + } + } + requestForToken.end() + } + else { + // Log in + do { + try response.redirect("https://github.com/login/oauth/authorize?client_id=\(clientId)&redirect_uri=\(callbackUrl)&response_type=code") + inProgress() + } + catch { + Log.error("Failed to redirect to \(name) login page") + } + } + } +}