{"action":"created","comment":{"url":"https://api.github.com/repos/curityio/aws-authenticator/pulls/comments/177955420","pull_request_review_id":107924986,"id":177955420,"diff_hunk":"@@ -38,118 +28,241 @@\n import se.curity.identityserver.sdk.authentication.AuthenticationResult;\n import se.curity.identityserver.sdk.authentication.AuthenticatorRequestHandler;\n import se.curity.identityserver.sdk.errors.ErrorCode;\n+import se.curity.identityserver.sdk.http.HttpRequest;\n+import se.curity.identityserver.sdk.http.HttpResponse;\n import se.curity.identityserver.sdk.service.ExceptionFactory;\n+import se.curity.identityserver.sdk.service.HttpClient;\n import se.curity.identityserver.sdk.service.Json;\n+import se.curity.identityserver.sdk.service.WebServiceClient;\n+import se.curity.identityserver.sdk.service.WebServiceClientFactory;\n import se.curity.identityserver.sdk.service.authentication.AuthenticatorInformationProvider;\n import se.curity.identityserver.sdk.web.Request;\n import se.curity.identityserver.sdk.web.Response;\n \n-import java.io.IOException;\n+import java.io.UnsupportedEncodingException;\n+import java.net.URI;\n+import java.net.URLEncoder;\n+import java.nio.charset.StandardCharsets;\n import java.util.Base64;\n+import java.util.HashMap;\n import java.util.Map;\n+import java.util.Objects;\n import java.util.Optional;\n \n-import static io.curity.identityserver.plugin.authentication.Constants.Params.*;\n-import static io.curity.identityserver.plugin.aws.authentication.Constants.*;\n import static se.curity.identityserver.sdk.attribute.ContextAttributes.AUTH_TIME;\n-import static se.curity.identityserver.sdk.attribute.ContextAttributes.SCOPE;\n \n-public class CallbackRequestHandler\n-        implements AuthenticatorRequestHandler<CallbackGetRequestModel> {\n-\n-\n-    private static final Logger _logger = LoggerFactory.getLogger(CallbackRequestHandler.class);\n+public class CallbackRequestHandler implements AuthenticatorRequestHandler<CallbackGetRequestModel>\n+{\n+    private final static Logger _logger = LoggerFactory.getLogger(CallbackRequestHandler.class);\n \n     private final ExceptionFactory _exceptionFactory;\n-    private final OAuthClient _oauthClient;\n     private final AWSAuthenticatorPluginConfig _config;\n-    private final HttpClient _client;\n-\n-    public CallbackRequestHandler(ExceptionFactory exceptionFactory,\n-                                  AuthenticatorInformationProvider provider,\n-                                  Json json,\n-                                  AWSAuthenticatorPluginConfig config) {\n-        _exceptionFactory = exceptionFactory;\n-        _oauthClient = new DefaultOAuthClient(exceptionFactory, provider, json, config.getSessionManager());\n-        _client = HttpClientBuilder.create().build();\n+    private final Json _json;\n+    private final AuthenticatorInformationProvider _authenticatorInformationProvider;\n+    private final WebServiceClientFactory _webServiceClientFactory;\n+\n+    public CallbackRequestHandler(AWSAuthenticatorPluginConfig config)\n+    {\n+        _exceptionFactory = config.getExceptionFactory();\n         _config = config;\n+        _json = config.getJson();\n+        _webServiceClientFactory = config.getWebServiceClientFactory();\n+        _authenticatorInformationProvider = config.getAuthenticatorInformationProvider();\n     }\n \n     @Override\n-    public CallbackGetRequestModel preProcess(Request request, Response response) {\n-        if (request.isGetRequest()) {\n+    public CallbackGetRequestModel preProcess(Request request, Response response)\n+    {\n+        if (request.isGetRequest())\n+        {\n             return new CallbackGetRequestModel(request);\n-        } else {\n+        }\n+        else\n+        {\n             throw _exceptionFactory.methodNotAllowed();\n         }\n     }\n \n     @Override\n-    public Optional<AuthenticationResult> get(CallbackGetRequestModel requestModel,\n-                                              Response response) {\n-        _oauthClient.redirectToAuthenticationOnError(requestModel.getRequest(), _config.id());\n+    public Optional<AuthenticationResult> post(CallbackGetRequestModel requestModel, Response response)\n+    {\n+        throw _exceptionFactory.methodNotAllowed();\n+    }\n \n-        Map<String, Object> tokenMap = getTokens(_config.getDomain() + _config.getTokenEndpoint().toString(),\n-                _config.getClientId(),\n-                _config.getClientSecret(),\n-                requestModel.getCode(),\n-                requestModel.getState());\n+    @Override\n+    public Optional<AuthenticationResult> get(CallbackGetRequestModel requestModel, Response response)\n+    {\n+        validateState(requestModel.getState());\n+        handleError(requestModel);\n+\n+        Map<String, Object> tokenResponseData = redeemCodeForTokens(requestModel);\n \n-        try {\n+        try\n+        {\n             //parse claims without need of key\n-            Map claimsMap = new JwtConsumerBuilder().setSkipAllValidators().setDisableRequireSignature().setSkipSignatureVerification().build().processToClaims(tokenMap.get(ID_TOKEN).toString()).getClaimsMap();\n+            String[] jwtParts = Objects.toString(tokenResponseData.get(\"id_token\")).split(\"\\\\.\", 3);\n+\n+            if (jwtParts.length < 2)\n+            {\n+                throw _exceptionFactory.internalServerException(ErrorCode.EXTERNAL_SERVICE_ERROR, \"Invalid JWT\");\n+            }\n+\n+            Base64.Decoder base64Url = Base64.getUrlDecoder();\n+            String body = new String(base64Url.decode(jwtParts[1]));\n+            Map<String, Object> claimsMap = _json.fromJson(body);\n \n-            String userId = claimsMap.get(USERNAME).toString();\n+            String userId = claimsMap.get(\"cognito:username\").toString();\n \n-            Attributes subjectAttributes = Attributes.of(Attribute.of(USERNAME, userId), Attribute.of(EMAIL, claimsMap.get(EMAIL).toString()));\n-            Attributes contextAttributes = Attributes.of(Attribute.of(PARAM_ACCESS_TOKEN, tokenMap.get(PARAM_ACCESS_TOKEN).toString()),\n-                    Attribute.of(AUTH_TIME, Long.valueOf(claimsMap.get(AUTH_TIME).toString())),\n-                    Attribute.of(EMAIL_VERIFIED, Boolean.valueOf(claimsMap.get(EMAIL_VERIFIED).toString())));\n+            Attributes subjectAttributes = Attributes.of(Attribute.of(\"username\", userId),\n+                    Attribute.of(\"email\", claimsMap.get(\"email\").toString()),\n+                    Attribute.of(\"phone_number\", (String) claimsMap.get(\"phone_number\"))\n+            );\n+            Attributes contextAttributes = Attributes.of(\n+                    Attribute.of(\"access_token\", tokenResponseData.get(\"access_token\").toString()),\n+                    Attribute.of(AUTH_TIME, (Long) claimsMap.get(AUTH_TIME)),\n+                    Attribute.of(\"email_verified\", (Boolean) claimsMap.get(\"email_verified\")),\n+                    Attribute.of(\"phone_number_verified\", (Boolean) claimsMap.get(\"phone_number_verified\"))\n+            );\n             AuthenticationAttributes attributes = AuthenticationAttributes.of(\n                     SubjectAttributes.of(userId, subjectAttributes),\n                     ContextAttributes.of(contextAttributes));\n             AuthenticationResult authenticationResult = new AuthenticationResult(attributes);\n             return Optional.ofNullable(authenticationResult);\n-        } catch (Exception e) {\n-            throw _exceptionFactory.internalServerException(ErrorCode.EXTERNAL_SERVICE_ERROR, \"Invalid token \" + e.getMessage());\n+        } catch (Exception e)\n+        {\n+            throw _exceptionFactory.internalServerException(ErrorCode.EXTERNAL_SERVICE_ERROR,\n+                    \"Invalid token \" + e.getMessage());\n         }\n     }\n \n-    @Override\n-    public Optional<AuthenticationResult> post(CallbackGetRequestModel requestModel,\n-                                               Response response) {\n-        throw _exceptionFactory.methodNotAllowed();\n-    }\n+    private Map<String, Object> redeemCodeForTokens(CallbackGetRequestModel requestModel)\n+    {\n+        HttpResponse tokenResponse = getWebServiceClient()\n+                .withPath(\"/oauth2/token\")\n+                .request()\n+                .contentType(\"application/x-www-form-urlencoded\")\n+                .body(getFormEncodedBodyFrom(createPostData(_config.getClientId(), _config.getClientSecret(),\n+                        requestModel.getCode(), requestModel.getRequestUrl())))\n+                .header(\"Authorization\", \"Basic \" +\n+                        Base64.getEncoder().encodeToString((_config.getClientId() + \":\" +\n+                                _config.getClientSecret()).getBytes()))\n+                .method(\"POST\")\n+                .response();\n+        int statusCode = tokenResponse.statusCode();\n \n-    public Map<String, Object> getTokens(String tokenEndpoint, String clientId, String clientSecret, String code, String state) {\n+        if (statusCode != 200)\n+        {\n+            if (_logger.isInfoEnabled())\n+            {\n+                _logger.info(\"Got error response from token endpoint: error = {}, {}\", statusCode,\n+                        tokenResponse.body(HttpResponse.asString()));\n+            }\n \n-        _oauthClient.validateState(state);\n+            throw _exceptionFactory.internalServerException(ErrorCode.EXTERNAL_SERVICE_ERROR);\n+        }\n \n-        try {\n-            ParamBuilder postData = new ParamBuilder();\n-            postData.addPair(PARAM_CODE, code)\n-                    .addPair(PARAM_GRANT_TYPE, PARAM_GRANT_TYPE_AUTHORIZATION_CODE)\n-                    .addPair(SCOPE, AWSAuthenticatorRequestHandler.getScope(_config))\n-                    .addPair(PARAM_REDIRECT_URI, _oauthClient.getCallbackUrl());\n-            UrlEncodedFormEntity data = new UrlEncodedFormEntity(postData.getPairs());\n+        return _json.fromJson(tokenResponse.body(HttpResponse.asString()));\n+    }\n \n-            HttpPost post = new HttpPost(tokenEndpoint);\n-            post.addHeader(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE);\n-            post.setHeader(HttpHeaders.AUTHORIZATION, \"Basic \" + Base64.getEncoder().encodeToString((clientId + \":\" + clientSecret).getBytes()));\n-            post.setEntity(data);\n+    private WebServiceClient getWebServiceClient()\n+    {\n+        Optional<HttpClient> httpClient = _config.getHttpClient();\n \n-            HttpResponse response = _client.execute(post);\n-            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {\n-                _logger.debug(\"Got error response from token endpoint {}\", response.getStatusLine());\n+        if (httpClient.isPresent())\n+        {\n+            return _webServiceClientFactory.create(httpClient.get()).withHost(_config.getDomain().getHost());\n+        }\n+        else\n+        {\n+            return _webServiceClientFactory.create(URI.create(_config.getDomain().toString()));\n+        }\n+    }\n \n-                throw _exceptionFactory.internalServerException(ErrorCode.INVALID_SERVER_STATE, \"INTERNAL SERVER ERROR\");\n+    private void handleError(CallbackGetRequestModel requestModel)\n+    {\n+        if (!Objects.isNull(requestModel.getError()))\n+        {\n+            if (\"access_denied\".equals(requestModel.getError()))\n+            {\n+                _logger.debug(\"Got an error from AWS: {} - {}\", requestModel.getError(),\n+                        requestModel.getErrorDescription());\n+\n+                throw _exceptionFactory.redirectException(\n+                        _authenticatorInformationProvider.getAuthenticationBaseUri().toASCIIString());","path":"src/main/java/io/curity/identityserver/plugin/aws/authentication/CallbackRequestHandler.java","position":254,"original_position":254,"commit_id":"a45d2d21646fe8b433596774567d0704f38647fa","original_commit_id":"a45d2d21646fe8b433596774567d0704f38647fa","user":{"login":"travisspencer","id":2499235,"avatar_url":"https://avatars3.githubusercontent.com/u/2499235?v=4","gravatar_id":"","url":"https://api.github.com/users/travisspencer","html_url":"https://github.com/travisspencer","followers_url":"https://api.github.com/users/travisspencer/followers","following_url":"https://api.github.com/users/travisspencer/following{/other_user}","gists_url":"https://api.github.com/users/travisspencer/gists{/gist_id}","starred_url":"https://api.github.com/users/travisspencer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/travisspencer/subscriptions","organizations_url":"https://api.github.com/users/travisspencer/orgs","repos_url":"https://api.github.com/users/travisspencer/repos","events_url":"https://api.github.com/users/travisspencer/events{/privacy}","received_events_url":"https://api.github.com/users/travisspencer/received_events","type":"User","site_admin":false},"body":"probably don't want to use `toASCIIString` here. If I name my authentication profile's authenticate endpoint `öööö`, this will break even though it's a valid URL.","created_at":"2018-03-29T05:18:20Z","updated_at":"2018-03-29T05:22:34Z","html_url":"https://github.com/curityio/aws-authenticator/pull/1#discussion_r177955420","pull_request_url":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1","author_association":"NONE","_links":{"self":{"href":"https://api.github.com/repos/curityio/aws-authenticator/pulls/comments/177955420"},"html":{"href":"https://github.com/curityio/aws-authenticator/pull/1#discussion_r177955420"},"pull_request":{"href":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1"}}},"pull_request":{"url":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1","id":178027243,"html_url":"https://github.com/curityio/aws-authenticator/pull/1","diff_url":"https://github.com/curityio/aws-authenticator/pull/1.diff","patch_url":"https://github.com/curityio/aws-authenticator/pull/1.patch","issue_url":"https://api.github.com/repos/curityio/aws-authenticator/issues/1","number":1,"state":"open","locked":false,"title":"complete auth flow and update readme.","user":{"login":"atifsaddique211f","id":8865972,"avatar_url":"https://avatars3.githubusercontent.com/u/8865972?v=4","gravatar_id":"","url":"https://api.github.com/users/atifsaddique211f","html_url":"https://github.com/atifsaddique211f","followers_url":"https://api.github.com/users/atifsaddique211f/followers","following_url":"https://api.github.com/users/atifsaddique211f/following{/other_user}","gists_url":"https://api.github.com/users/atifsaddique211f/gists{/gist_id}","starred_url":"https://api.github.com/users/atifsaddique211f/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/atifsaddique211f/subscriptions","organizations_url":"https://api.github.com/users/atifsaddique211f/orgs","repos_url":"https://api.github.com/users/atifsaddique211f/repos","events_url":"https://api.github.com/users/atifsaddique211f/events{/privacy}","received_events_url":"https://api.github.com/users/atifsaddique211f/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-03-28T12:47:59Z","updated_at":"2018-03-29T05:22:34Z","closed_at":null,"merged_at":null,"merge_commit_sha":"190cb51a24d7405d50be5f8be5089060b7e326ed","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1/commits","review_comments_url":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1/comments","review_comment_url":"https://api.github.com/repos/curityio/aws-authenticator/pulls/comments{/number}","comments_url":"https://api.github.com/repos/curityio/aws-authenticator/issues/1/comments","statuses_url":"https://api.github.com/repos/curityio/aws-authenticator/statuses/a45d2d21646fe8b433596774567d0704f38647fa","head":{"label":"curityio:dev","ref":"dev","sha":"a45d2d21646fe8b433596774567d0704f38647fa","user":{"login":"curityio","id":22588857,"avatar_url":"https://avatars3.githubusercontent.com/u/22588857?v=4","gravatar_id":"","url":"https://api.github.com/users/curityio","html_url":"https://github.com/curityio","followers_url":"https://api.github.com/users/curityio/followers","following_url":"https://api.github.com/users/curityio/following{/other_user}","gists_url":"https://api.github.com/users/curityio/gists{/gist_id}","starred_url":"https://api.github.com/users/curityio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/curityio/subscriptions","organizations_url":"https://api.github.com/users/curityio/orgs","repos_url":"https://api.github.com/users/curityio/repos","events_url":"https://api.github.com/users/curityio/events{/privacy}","received_events_url":"https://api.github.com/users/curityio/received_events","type":"Organization","site_admin":false},"repo":{"id":114043907,"name":"aws-authenticator","full_name":"curityio/aws-authenticator","owner":{"login":"curityio","id":22588857,"avatar_url":"https://avatars3.githubusercontent.com/u/22588857?v=4","gravatar_id":"","url":"https://api.github.com/users/curityio","html_url":"https://github.com/curityio","followers_url":"https://api.github.com/users/curityio/followers","following_url":"https://api.github.com/users/curityio/following{/other_user}","gists_url":"https://api.github.com/users/curityio/gists{/gist_id}","starred_url":"https://api.github.com/users/curityio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/curityio/subscriptions","organizations_url":"https://api.github.com/users/curityio/orgs","repos_url":"https://api.github.com/users/curityio/repos","events_url":"https://api.github.com/users/curityio/events{/privacy}","received_events_url":"https://api.github.com/users/curityio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/curityio/aws-authenticator","description":"AWS Cognito oauth authenticator that can be used with any Java-based Web API","fork":false,"url":"https://api.github.com/repos/curityio/aws-authenticator","forks_url":"https://api.github.com/repos/curityio/aws-authenticator/forks","keys_url":"https://api.github.com/repos/curityio/aws-authenticator/keys{/key_id}","collaborators_url":"https://api.github.com/repos/curityio/aws-authenticator/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/curityio/aws-authenticator/teams","hooks_url":"https://api.github.com/repos/curityio/aws-authenticator/hooks","issue_events_url":"https://api.github.com/repos/curityio/aws-authenticator/issues/events{/number}","events_url":"https://api.github.com/repos/curityio/aws-authenticator/events","assignees_url":"https://api.github.com/repos/curityio/aws-authenticator/assignees{/user}","branches_url":"https://api.github.com/repos/curityio/aws-authenticator/branches{/branch}","tags_url":"https://api.github.com/repos/curityio/aws-authenticator/tags","blobs_url":"https://api.github.com/repos/curityio/aws-authenticator/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/curityio/aws-authenticator/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/curityio/aws-authenticator/git/refs{/sha}","trees_url":"https://api.github.com/repos/curityio/aws-authenticator/git/trees{/sha}","statuses_url":"https://api.github.com/repos/curityio/aws-authenticator/statuses/{sha}","languages_url":"https://api.github.com/repos/curityio/aws-authenticator/languages","stargazers_url":"https://api.github.com/repos/curityio/aws-authenticator/stargazers","contributors_url":"https://api.github.com/repos/curityio/aws-authenticator/contributors","subscribers_url":"https://api.github.com/repos/curityio/aws-authenticator/subscribers","subscription_url":"https://api.github.com/repos/curityio/aws-authenticator/subscription","commits_url":"https://api.github.com/repos/curityio/aws-authenticator/commits{/sha}","git_commits_url":"https://api.github.com/repos/curityio/aws-authenticator/git/commits{/sha}","comments_url":"https://api.github.com/repos/curityio/aws-authenticator/comments{/number}","issue_comment_url":"https://api.github.com/repos/curityio/aws-authenticator/issues/comments{/number}","contents_url":"https://api.github.com/repos/curityio/aws-authenticator/contents/{+path}","compare_url":"https://api.github.com/repos/curityio/aws-authenticator/compare/{base}...{head}","merges_url":"https://api.github.com/repos/curityio/aws-authenticator/merges","archive_url":"https://api.github.com/repos/curityio/aws-authenticator/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/curityio/aws-authenticator/downloads","issues_url":"https://api.github.com/repos/curityio/aws-authenticator/issues{/number}","pulls_url":"https://api.github.com/repos/curityio/aws-authenticator/pulls{/number}","milestones_url":"https://api.github.com/repos/curityio/aws-authenticator/milestones{/number}","notifications_url":"https://api.github.com/repos/curityio/aws-authenticator/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/curityio/aws-authenticator/labels{/name}","releases_url":"https://api.github.com/repos/curityio/aws-authenticator/releases{/id}","deployments_url":"https://api.github.com/repos/curityio/aws-authenticator/deployments","created_at":"2017-12-12T22:04:52Z","updated_at":"2018-02-05T20:42:06Z","pushed_at":"2018-03-28T13:12:52Z","git_url":"git://github.com/curityio/aws-authenticator.git","ssh_url":"git@github.com:curityio/aws-authenticator.git","clone_url":"https://github.com/curityio/aws-authenticator.git","svn_url":"https://github.com/curityio/aws-authenticator","homepage":null,"size":395,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"curityio:master","ref":"master","sha":"4b56971b120682c482f86ad27915f28ef40b3252","user":{"login":"curityio","id":22588857,"avatar_url":"https://avatars3.githubusercontent.com/u/22588857?v=4","gravatar_id":"","url":"https://api.github.com/users/curityio","html_url":"https://github.com/curityio","followers_url":"https://api.github.com/users/curityio/followers","following_url":"https://api.github.com/users/curityio/following{/other_user}","gists_url":"https://api.github.com/users/curityio/gists{/gist_id}","starred_url":"https://api.github.com/users/curityio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/curityio/subscriptions","organizations_url":"https://api.github.com/users/curityio/orgs","repos_url":"https://api.github.com/users/curityio/repos","events_url":"https://api.github.com/users/curityio/events{/privacy}","received_events_url":"https://api.github.com/users/curityio/received_events","type":"Organization","site_admin":false},"repo":{"id":114043907,"name":"aws-authenticator","full_name":"curityio/aws-authenticator","owner":{"login":"curityio","id":22588857,"avatar_url":"https://avatars3.githubusercontent.com/u/22588857?v=4","gravatar_id":"","url":"https://api.github.com/users/curityio","html_url":"https://github.com/curityio","followers_url":"https://api.github.com/users/curityio/followers","following_url":"https://api.github.com/users/curityio/following{/other_user}","gists_url":"https://api.github.com/users/curityio/gists{/gist_id}","starred_url":"https://api.github.com/users/curityio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/curityio/subscriptions","organizations_url":"https://api.github.com/users/curityio/orgs","repos_url":"https://api.github.com/users/curityio/repos","events_url":"https://api.github.com/users/curityio/events{/privacy}","received_events_url":"https://api.github.com/users/curityio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/curityio/aws-authenticator","description":"AWS Cognito oauth authenticator that can be used with any Java-based Web API","fork":false,"url":"https://api.github.com/repos/curityio/aws-authenticator","forks_url":"https://api.github.com/repos/curityio/aws-authenticator/forks","keys_url":"https://api.github.com/repos/curityio/aws-authenticator/keys{/key_id}","collaborators_url":"https://api.github.com/repos/curityio/aws-authenticator/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/curityio/aws-authenticator/teams","hooks_url":"https://api.github.com/repos/curityio/aws-authenticator/hooks","issue_events_url":"https://api.github.com/repos/curityio/aws-authenticator/issues/events{/number}","events_url":"https://api.github.com/repos/curityio/aws-authenticator/events","assignees_url":"https://api.github.com/repos/curityio/aws-authenticator/assignees{/user}","branches_url":"https://api.github.com/repos/curityio/aws-authenticator/branches{/branch}","tags_url":"https://api.github.com/repos/curityio/aws-authenticator/tags","blobs_url":"https://api.github.com/repos/curityio/aws-authenticator/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/curityio/aws-authenticator/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/curityio/aws-authenticator/git/refs{/sha}","trees_url":"https://api.github.com/repos/curityio/aws-authenticator/git/trees{/sha}","statuses_url":"https://api.github.com/repos/curityio/aws-authenticator/statuses/{sha}","languages_url":"https://api.github.com/repos/curityio/aws-authenticator/languages","stargazers_url":"https://api.github.com/repos/curityio/aws-authenticator/stargazers","contributors_url":"https://api.github.com/repos/curityio/aws-authenticator/contributors","subscribers_url":"https://api.github.com/repos/curityio/aws-authenticator/subscribers","subscription_url":"https://api.github.com/repos/curityio/aws-authenticator/subscription","commits_url":"https://api.github.com/repos/curityio/aws-authenticator/commits{/sha}","git_commits_url":"https://api.github.com/repos/curityio/aws-authenticator/git/commits{/sha}","comments_url":"https://api.github.com/repos/curityio/aws-authenticator/comments{/number}","issue_comment_url":"https://api.github.com/repos/curityio/aws-authenticator/issues/comments{/number}","contents_url":"https://api.github.com/repos/curityio/aws-authenticator/contents/{+path}","compare_url":"https://api.github.com/repos/curityio/aws-authenticator/compare/{base}...{head}","merges_url":"https://api.github.com/repos/curityio/aws-authenticator/merges","archive_url":"https://api.github.com/repos/curityio/aws-authenticator/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/curityio/aws-authenticator/downloads","issues_url":"https://api.github.com/repos/curityio/aws-authenticator/issues{/number}","pulls_url":"https://api.github.com/repos/curityio/aws-authenticator/pulls{/number}","milestones_url":"https://api.github.com/repos/curityio/aws-authenticator/milestones{/number}","notifications_url":"https://api.github.com/repos/curityio/aws-authenticator/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/curityio/aws-authenticator/labels{/name}","releases_url":"https://api.github.com/repos/curityio/aws-authenticator/releases{/id}","deployments_url":"https://api.github.com/repos/curityio/aws-authenticator/deployments","created_at":"2017-12-12T22:04:52Z","updated_at":"2018-02-05T20:42:06Z","pushed_at":"2018-03-28T13:12:52Z","git_url":"git://github.com/curityio/aws-authenticator.git","ssh_url":"git@github.com:curityio/aws-authenticator.git","clone_url":"https://github.com/curityio/aws-authenticator.git","svn_url":"https://github.com/curityio/aws-authenticator","homepage":null,"size":395,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1"},"html":{"href":"https://github.com/curityio/aws-authenticator/pull/1"},"issue":{"href":"https://api.github.com/repos/curityio/aws-authenticator/issues/1"},"comments":{"href":"https://api.github.com/repos/curityio/aws-authenticator/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/curityio/aws-authenticator/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/curityio/aws-authenticator/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/curityio/aws-authenticator/statuses/a45d2d21646fe8b433596774567d0704f38647fa"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"travisspencer"}}