diff --git a/src/main/java/org/kohsuke/github/GHOTPRequiredException.java b/src/main/java/org/kohsuke/github/GHOTPRequiredException.java new file mode 100644 index 000000000..f7d935bd5 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHOTPRequiredException.java @@ -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 { +//... +} diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index fcd4c171a..94ffd6f0b 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -44,6 +44,7 @@ import java.text.SimpleDateFormat; 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; @@ -702,7 +703,31 @@ public class GitHub { 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 Documentation + */ + public GHAuthorization createToken(Collection scope, String note, String noteUrl, Supplier 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 docs */ diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 4078081cc..12765bd45 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -763,8 +763,13 @@ class Requester { 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); diff --git a/src/test/java/org/kohsuke/github/Github2faTest.java b/src/test/java/org/kohsuke/github/Github2faTest.java new file mode 100644 index 000000000..ebac9e684 --- /dev/null +++ b/src/test/java/org/kohsuke/github/Github2faTest.java @@ -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 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