Skip to content

Commit

Permalink
Merge pull request #603 from madhephaestus/master
Browse files Browse the repository at this point in the history
Add Functionality of OTP to support user 2fa
  • Loading branch information
bitwiseman committed Nov 13, 2019
2 parents adc436a + e325bf7 commit d23c718
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 3 deletions.
10 changes: 10 additions & 0 deletions src/main/java/org/kohsuke/github/GHOTPRequiredException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.kohsuke.github;
/**
* This exception is thrown when GitHub is requesting an OTP from the user
*
* @author Kevin Harrington mad.hephaestus@gmail.com
*
*/
public class GHOTPRequiredException extends GHIOException {
//...
}
27 changes: 26 additions & 1 deletion src/main/java/org/kohsuke/github/GitHub.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
import java.util.logging.Logger;

import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
Expand Down Expand Up @@ -702,7 +703,31 @@ public GHAuthorization createToken(Collection<String> scope, String note, String

return requester.method("POST").to("/authorizations", GHAuthorization.class).wrap(this);
}

/**
* Creates a new authorization using an OTP.
*
* Start by running createToken, if exception is thrown, prompt for OTP from user
*
* Once OTP is received, call this token request
*
* The token created can be then used for {@link GitHub#connectUsingOAuth(String)} in the future.
*
* @see <a href="http://developer.github.com/v3/oauth/#create-a-new-authorization">Documentation</a>
*/
public GHAuthorization createToken(Collection<String> scope, String note, String noteUrl, Supplier<String> OTP) throws IOException{
try {
return createToken(scope, note, noteUrl);
}catch (GHOTPRequiredException ex){
String OTPstring=OTP.get();
Requester requester = new Requester(this)
.with("scopes", scope)
.with("note", note)
.with("note_url", noteUrl);
// Add the OTP from the user
requester.setHeader("x-github-otp", OTPstring);
return requester.method("POST").to("/authorizations", GHAuthorization.class).wrap(this);
}
}
/**
* @see <a href="https://developer.github.com/v3/oauth_authorizations/#get-or-create-an-authorization-for-a-specific-app">docs</a>
*/
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/org/kohsuke/github/Requester.java
Original file line number Diff line number Diff line change
Expand Up @@ -763,8 +763,13 @@ private InputStream wrapStream(InputStream in) throws IOException {
IOUtils.closeQuietly(es);
}
}
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) // 401 / Unauthorized == bad creds
throw e;
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) // 401 Unauthorized == bad creds or OTP request
// In the case of a user with 2fa enabled, a header with X-GitHub-OTP
// will be returned indicating the user needs to respond with an otp
if(uc.getHeaderField("X-GitHub-OTP") != null)
throw (IOException) new GHOTPRequiredException().withResponseHeaderFields(uc).initCause(e);
else
throw e; // usually org.kohsuke.github.HttpException (which extends IOException)

if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) {
root.rateLimitHandler.onError(e,uc);
Expand Down
38 changes: 38 additions & 0 deletions src/test/java/org/kohsuke/github/Github2faTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.kohsuke.github;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import org.junit.Test;
/**
* @author Kevin Harrington mad.hephaestus@gmail.com
*/
public class Github2faTest extends AbstractGitHubWireMockTest {

@Test
public void test2faToken() throws IOException {
assertFalse("Test only valid when not proxying", mockGitHub.isUseProxy());

List<String> asList = Arrays.asList("repo", "gist", "write:packages", "read:packages", "delete:packages",
"user", "delete_repo");
String nameOfToken = "Test2faTokenCreate";//+timestamp;// use time stamp to ensure the token creations do not collide with older tokens

GHAuthorization token=gitHub.createToken(
asList,
nameOfToken,
"this is a test token created by a unit test", () -> {
String data = "111878";
// TO UPDATE run this in debugger mode, put a breakpoint here, and enter the OTP you get into the value of Data
return data;
});
assert token!=null;
for(int i=0;i<asList.size();i++) {
assertTrue(token.getScopes().get(i).contentEquals(asList.get(i)));
}

String p = token.getToken();

assert p!=null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"id": 350917110,
"url": "https://api.github.com/authorizations/350917110",
"app": {
"name": "Test2faTokenCreate",
"url": "this is a test token created by a unit test",
"client_id": "00000000000000000000"
},
"token": "63042a99d88bf138e6d6cf5788e0dc4e7a5d7309",
"hashed_token": "12b727a23cad7c5a5caabb806d88e722794dede98464aed7f77cbc00dbf031a2",
"token_last_eight": "7a5d7309",
"note": "Test2faTokenCreate",
"note_url": "this is a test token created by a unit test",
"created_at": "2019-11-12T23:04:13Z",
"updated_at": "2019-11-12T23:04:13Z",
"scopes": [
"repo",
"gist",
"write:packages",
"read:packages",
"delete:packages",
"user",
"delete_repo"
],
"fingerprint": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"id": "cdda4d8e-7412-4b68-84ea-4d691fc8ccb8",
"name": "authorizations",
"request": {
"url": "/authorizations",
"method": "POST",
"bodyPatterns": [
{
"equalToJson": "{\"note\":\"Test2faTokenCreate\",\"note_url\":\"this is a test token created by a unit test\",\"scopes\":[\"repo\",\"gist\",\"write:packages\",\"read:packages\",\"delete:packages\",\"user\",\"delete_repo\"]}",
"ignoreArrayOrder": true,
"ignoreExtraElements": true
}
]
},
"response": {
"status": 401,
"body": "{\"message\":\"Must specify two-factor authentication OTP code.\",\"documentation_url\":\"https://developer.github.com/v3/auth#working-with-two-factor-authentication\"}",
"headers": {
"Server": "GitHub.com",
"Date": "Tue, 12 Nov 2019 23:03:53 GMT",
"Content-Type": "application/json; charset=utf-8",
"Status": "401 Unauthorized",
"X-GitHub-OTP": "required; sms",
"X-GitHub-Media-Type": "unknown, github.v3",
"X-RateLimit-Limit": "60",
"X-RateLimit-Remaining": "59",
"X-RateLimit-Reset": "1573603433",
"Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type",
"Access-Control-Allow-Origin": "*",
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
"X-Frame-Options": "deny",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
"Content-Security-Policy": "default-src 'none'",
"X-GitHub-Request-Id": "EA5C:557C:1271013:29480B4:5DCB3A59"
}
},
"uuid": "cdda4d8e-7412-4b68-84ea-4d691fc8ccb8",
"persistent": true,
"scenarioName": "scenario-1-authorizations",
"requiredScenarioState": "Started",
"newScenarioState": "scenario-1-authorizations-2",
"insertionIndex": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"id": "3f50c6b0-2730-45e7-9c94-f4f71bf61db2",
"name": "authorizations",
"request": {
"url": "/authorizations",
"method": "POST",
"bodyPatterns": [
{
"equalToJson": "{\"note\":\"Test2faTokenCreate\",\"note_url\":\"this is a test token created by a unit test\",\"scopes\":[\"repo\",\"gist\",\"write:packages\",\"read:packages\",\"delete:packages\",\"user\",\"delete_repo\"]}",
"ignoreArrayOrder": true,
"ignoreExtraElements": true
}
]
},
"response": {
"status": 201,
"bodyFileName": "authorizations-3f50c6b0-2730-45e7-9c94-f4f71bf61db2.json",
"headers": {
"Server": "GitHub.com",
"Date": "Tue, 12 Nov 2019 23:04:13 GMT",
"Content-Type": "application/json; charset=utf-8",
"Status": "201 Created",
"X-RateLimit-Limit": "5000",
"X-RateLimit-Remaining": "4999",
"X-RateLimit-Reset": "1573603453",
"Cache-Control": "private, max-age=60, s-maxage=60",
"Vary": "Accept, Authorization, Cookie, X-GitHub-OTP",
"ETag": "\"a13c56b386b13166f07b380d02243b12\"",
"Location": "https://api.github.com/authorizations/350917110",
"X-GitHub-Media-Type": "unknown, github.v3",
"Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type",
"Access-Control-Allow-Origin": "*",
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
"X-Frame-Options": "deny",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
"Content-Security-Policy": "default-src 'none'",
"X-GitHub-Request-Id": "EA5C:557C:1271445:29480C8:5DCB3A59"
}
},
"uuid": "3f50c6b0-2730-45e7-9c94-f4f71bf61db2",
"persistent": true,
"scenarioName": "scenario-1-authorizations",
"requiredScenarioState": "scenario-1-authorizations-2",
"insertionIndex": 2
}

0 comments on commit d23c718

Please sign in to comment.