@@ -88,7 +88,9 @@ of this software and associated documentation files (the "Software"), to deal
88
88
import java .io .IOException ;
89
89
import java .net .InetSocketAddress ;
90
90
import java .net .Proxy ;
91
+ import java .security .SecureRandom ;
91
92
import java .util .Arrays ;
93
+ import java .util .Base64 ;
92
94
import java .util .HashSet ;
93
95
import java .util .Set ;
94
96
import java .util .logging .Logger ;
@@ -337,6 +339,7 @@ public String getOauthScopes() {
337
339
338
340
public HttpResponse doCommenceLogin (StaplerRequest request , @ QueryParameter String from , @ Header ("Referer" ) final String referer )
339
341
throws IOException {
342
+ final String state = getSecureRandomString ();
340
343
String redirectOnFinish ;
341
344
if (from != null && Util .isSafeToRedirectTo (from )) {
342
345
redirectOnFinish = from ;
@@ -347,18 +350,19 @@ public HttpResponse doCommenceLogin(StaplerRequest request, @QueryParameter Stri
347
350
}
348
351
349
352
request .getSession ().setAttribute (REFERER_ATTRIBUTE , redirectOnFinish );
353
+ request .getSession ().setAttribute (STATE_ATTRIBUTE , state );
350
354
351
355
Set <String > scopes = new HashSet <>();
352
356
for (GitHubOAuthScope s : getJenkins ().getExtensionList (GitHubOAuthScope .class )) {
353
357
scopes .addAll (s .getScopesToRequest ());
354
358
}
355
359
String suffix ="" ;
356
360
if (!scopes .isEmpty ()) {
357
- suffix = "&scope=" +Util .join (scopes ,"," );
361
+ suffix = "&scope=" +Util .join (scopes ,"," )+ "&state=" + state ;
358
362
} else {
359
363
// We need repo scope in order to access private repos
360
364
// See https://developer.github.com/v3/oauth/#scopes
361
- suffix = "&scope=" + oauthScopes ;
365
+ suffix = "&scope=" + oauthScopes + "&state=" + state ;
362
366
}
363
367
364
368
return new HttpRedirect (githubWebUri + "/login/oauth/authorize?client_id="
@@ -372,13 +376,27 @@ public HttpResponse doCommenceLogin(StaplerRequest request, @QueryParameter Stri
372
376
public HttpResponse doFinishLogin (StaplerRequest request )
373
377
throws IOException {
374
378
String code = request .getParameter ("code" );
379
+ String state = request .getParameter (STATE_ATTRIBUTE );
375
380
String referer = (String )request .getSession ().getAttribute (REFERER_ATTRIBUTE );
381
+ String expectedState = (String )request .getSession ().getAttribute (STATE_ATTRIBUTE );
376
382
377
383
if (code == null || code .trim ().length () == 0 ) {
378
384
Log .info ("doFinishLogin: missing code." );
379
385
return HttpResponses .redirectToContextRoot ();
380
386
}
381
387
388
+ if (state == null ){
389
+ Log .info ("doFinishLogin: missing state parameter from Github response." );
390
+ return HttpResponses .redirectToContextRoot ();
391
+ } else if (expectedState == null ){
392
+ Log .info ("doFinishLogin: missing state parameter from user's session." );
393
+ return HttpResponses .redirectToContextRoot ();
394
+ } else if (!state .equals (expectedState )){
395
+ Log .info ("state parameter value [" +state +"] does not match the expected one [" +expectedState +"]" );
396
+ return HttpResponses .redirectToContextRoot ();
397
+ }
398
+
399
+
382
400
String accessToken = getAccessToken (code );
383
401
384
402
if (accessToken != null && accessToken .trim ().length () > 0 ) {
@@ -460,6 +478,16 @@ private String getAccessToken(@Nonnull String code) throws IOException {
460
478
return null ;
461
479
}
462
480
481
+ /**
482
+ * Generates a random 20 byte String that conforms to the <a href="https://tools.ietf.org/html/rfc6749#section-10.10">specification</a>
483
+ * requirements
484
+ * @return a string that can be used as a state parameter
485
+ */
486
+ private String getSecureRandomString () {
487
+ final byte [] bytes = new byte [20 ];
488
+ SECURE_RANDOM .nextBytes (bytes );
489
+ return BASE64_ENCODER .encodeToString (bytes );
490
+ }
463
491
/**
464
492
* Returns the proxy to be used when connecting to the given URI.
465
493
*/
@@ -789,6 +817,11 @@ static Jenkins getJenkins() {
789
817
private static final Logger LOGGER = Logger .getLogger (GithubSecurityRealm .class .getName ());
790
818
791
819
private static final String REFERER_ATTRIBUTE = GithubSecurityRealm .class .getName ()+".referer" ;
820
+ private static final String STATE_ATTRIBUTE = "state" ;
821
+
822
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom ();
823
+
824
+ private static final Base64 .Encoder BASE64_ENCODER = Base64 .getUrlEncoder ().withoutPadding ();
792
825
793
826
/**
794
827
* Asks for the password.
0 commit comments