Skip to content

Commit d23c718

Browse files
authored
Merge pull request hub4j#603 from madhephaestus/master
Add Functionality of OTP to support user 2fa
2 parents adc436a + e325bf7 commit d23c718

7 files changed

Lines changed: 199 additions & 3 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.kohsuke.github;
2+
/**
3+
* This exception is thrown when GitHub is requesting an OTP from the user
4+
*
5+
* @author Kevin Harrington mad.hephaestus@gmail.com
6+
*
7+
*/
8+
public class GHOTPRequiredException extends GHIOException {
9+
//...
10+
}

src/main/java/org/kohsuke/github/GitHub.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import java.util.*;
4545
import java.util.concurrent.ConcurrentHashMap;
4646
import java.util.concurrent.ConcurrentMap;
47+
import java.util.function.Supplier;
4748
import java.util.logging.Logger;
4849

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

703704
return requester.method("POST").to("/authorizations", GHAuthorization.class).wrap(this);
704705
}
705-
706+
/**
707+
* Creates a new authorization using an OTP.
708+
*
709+
* Start by running createToken, if exception is thrown, prompt for OTP from user
710+
*
711+
* Once OTP is received, call this token request
712+
*
713+
* The token created can be then used for {@link GitHub#connectUsingOAuth(String)} in the future.
714+
*
715+
* @see <a href="http://developer.github.com/v3/oauth/#create-a-new-authorization">Documentation</a>
716+
*/
717+
public GHAuthorization createToken(Collection<String> scope, String note, String noteUrl, Supplier<String> OTP) throws IOException{
718+
try {
719+
return createToken(scope, note, noteUrl);
720+
}catch (GHOTPRequiredException ex){
721+
String OTPstring=OTP.get();
722+
Requester requester = new Requester(this)
723+
.with("scopes", scope)
724+
.with("note", note)
725+
.with("note_url", noteUrl);
726+
// Add the OTP from the user
727+
requester.setHeader("x-github-otp", OTPstring);
728+
return requester.method("POST").to("/authorizations", GHAuthorization.class).wrap(this);
729+
}
730+
}
706731
/**
707732
* @see <a href="https://developer.github.com/v3/oauth_authorizations/#get-or-create-an-authorization-for-a-specific-app">docs</a>
708733
*/

src/main/java/org/kohsuke/github/Requester.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,8 +763,13 @@ private InputStream wrapStream(InputStream in) throws IOException {
763763
IOUtils.closeQuietly(es);
764764
}
765765
}
766-
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) // 401 / Unauthorized == bad creds
767-
throw e;
766+
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) // 401 Unauthorized == bad creds or OTP request
767+
// In the case of a user with 2fa enabled, a header with X-GitHub-OTP
768+
// will be returned indicating the user needs to respond with an otp
769+
if(uc.getHeaderField("X-GitHub-OTP") != null)
770+
throw (IOException) new GHOTPRequiredException().withResponseHeaderFields(uc).initCause(e);
771+
else
772+
throw e; // usually org.kohsuke.github.HttpException (which extends IOException)
768773

769774
if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) {
770775
root.rateLimitHandler.onError(e,uc);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.kohsuke.github;
2+
3+
import java.io.IOException;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
7+
import org.junit.Test;
8+
/**
9+
* @author Kevin Harrington mad.hephaestus@gmail.com
10+
*/
11+
public class Github2faTest extends AbstractGitHubWireMockTest {
12+
13+
@Test
14+
public void test2faToken() throws IOException {
15+
assertFalse("Test only valid when not proxying", mockGitHub.isUseProxy());
16+
17+
List<String> asList = Arrays.asList("repo", "gist", "write:packages", "read:packages", "delete:packages",
18+
"user", "delete_repo");
19+
String nameOfToken = "Test2faTokenCreate";//+timestamp;// use time stamp to ensure the token creations do not collide with older tokens
20+
21+
GHAuthorization token=gitHub.createToken(
22+
asList,
23+
nameOfToken,
24+
"this is a test token created by a unit test", () -> {
25+
String data = "111878";
26+
// TO UPDATE run this in debugger mode, put a breakpoint here, and enter the OTP you get into the value of Data
27+
return data;
28+
});
29+
assert token!=null;
30+
for(int i=0;i<asList.size();i++) {
31+
assertTrue(token.getScopes().get(i).contentEquals(asList.get(i)));
32+
}
33+
34+
String p = token.getToken();
35+
36+
assert p!=null;
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"id": 350917110,
3+
"url": "https://api.github.com/authorizations/350917110",
4+
"app": {
5+
"name": "Test2faTokenCreate",
6+
"url": "this is a test token created by a unit test",
7+
"client_id": "00000000000000000000"
8+
},
9+
"token": "63042a99d88bf138e6d6cf5788e0dc4e7a5d7309",
10+
"hashed_token": "12b727a23cad7c5a5caabb806d88e722794dede98464aed7f77cbc00dbf031a2",
11+
"token_last_eight": "7a5d7309",
12+
"note": "Test2faTokenCreate",
13+
"note_url": "this is a test token created by a unit test",
14+
"created_at": "2019-11-12T23:04:13Z",
15+
"updated_at": "2019-11-12T23:04:13Z",
16+
"scopes": [
17+
"repo",
18+
"gist",
19+
"write:packages",
20+
"read:packages",
21+
"delete:packages",
22+
"user",
23+
"delete_repo"
24+
],
25+
"fingerprint": null
26+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"id": "cdda4d8e-7412-4b68-84ea-4d691fc8ccb8",
3+
"name": "authorizations",
4+
"request": {
5+
"url": "/authorizations",
6+
"method": "POST",
7+
"bodyPatterns": [
8+
{
9+
"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\"]}",
10+
"ignoreArrayOrder": true,
11+
"ignoreExtraElements": true
12+
}
13+
]
14+
},
15+
"response": {
16+
"status": 401,
17+
"body": "{\"message\":\"Must specify two-factor authentication OTP code.\",\"documentation_url\":\"https://developer.github.com/v3/auth#working-with-two-factor-authentication\"}",
18+
"headers": {
19+
"Server": "GitHub.com",
20+
"Date": "Tue, 12 Nov 2019 23:03:53 GMT",
21+
"Content-Type": "application/json; charset=utf-8",
22+
"Status": "401 Unauthorized",
23+
"X-GitHub-OTP": "required; sms",
24+
"X-GitHub-Media-Type": "unknown, github.v3",
25+
"X-RateLimit-Limit": "60",
26+
"X-RateLimit-Remaining": "59",
27+
"X-RateLimit-Reset": "1573603433",
28+
"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",
29+
"Access-Control-Allow-Origin": "*",
30+
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
31+
"X-Frame-Options": "deny",
32+
"X-Content-Type-Options": "nosniff",
33+
"X-XSS-Protection": "1; mode=block",
34+
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
35+
"Content-Security-Policy": "default-src 'none'",
36+
"X-GitHub-Request-Id": "EA5C:557C:1271013:29480B4:5DCB3A59"
37+
}
38+
},
39+
"uuid": "cdda4d8e-7412-4b68-84ea-4d691fc8ccb8",
40+
"persistent": true,
41+
"scenarioName": "scenario-1-authorizations",
42+
"requiredScenarioState": "Started",
43+
"newScenarioState": "scenario-1-authorizations-2",
44+
"insertionIndex": 1
45+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"id": "3f50c6b0-2730-45e7-9c94-f4f71bf61db2",
3+
"name": "authorizations",
4+
"request": {
5+
"url": "/authorizations",
6+
"method": "POST",
7+
"bodyPatterns": [
8+
{
9+
"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\"]}",
10+
"ignoreArrayOrder": true,
11+
"ignoreExtraElements": true
12+
}
13+
]
14+
},
15+
"response": {
16+
"status": 201,
17+
"bodyFileName": "authorizations-3f50c6b0-2730-45e7-9c94-f4f71bf61db2.json",
18+
"headers": {
19+
"Server": "GitHub.com",
20+
"Date": "Tue, 12 Nov 2019 23:04:13 GMT",
21+
"Content-Type": "application/json; charset=utf-8",
22+
"Status": "201 Created",
23+
"X-RateLimit-Limit": "5000",
24+
"X-RateLimit-Remaining": "4999",
25+
"X-RateLimit-Reset": "1573603453",
26+
"Cache-Control": "private, max-age=60, s-maxage=60",
27+
"Vary": "Accept, Authorization, Cookie, X-GitHub-OTP",
28+
"ETag": "\"a13c56b386b13166f07b380d02243b12\"",
29+
"Location": "https://api.github.com/authorizations/350917110",
30+
"X-GitHub-Media-Type": "unknown, github.v3",
31+
"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",
32+
"Access-Control-Allow-Origin": "*",
33+
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
34+
"X-Frame-Options": "deny",
35+
"X-Content-Type-Options": "nosniff",
36+
"X-XSS-Protection": "1; mode=block",
37+
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
38+
"Content-Security-Policy": "default-src 'none'",
39+
"X-GitHub-Request-Id": "EA5C:557C:1271445:29480C8:5DCB3A59"
40+
}
41+
},
42+
"uuid": "3f50c6b0-2730-45e7-9c94-f4f71bf61db2",
43+
"persistent": true,
44+
"scenarioName": "scenario-1-authorizations",
45+
"requiredScenarioState": "scenario-1-authorizations-2",
46+
"insertionIndex": 2
47+
}

0 commit comments

Comments
 (0)