@@ -88,6 +88,7 @@ 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 ;
92
93
import java .util .HashSet ;
93
94
import java .util .Set ;
@@ -337,6 +338,9 @@ public String getOauthScopes() {
337
338
338
339
public HttpResponse doCommenceLogin (StaplerRequest request , @ QueryParameter String from , @ Header ("Referer" ) final String referer )
339
340
throws IOException {
341
+ // https://tools.ietf.org/html/rfc6749#section-10.10 dictates that probability that an attacker guesses the string
342
+ // SHOULD be less than or equal to 2^(-160) and our Strings consist of 65 chars. (65^27 ~= 2^160)
343
+ final String state = getSecureRandomString (27 );
340
344
String redirectOnFinish ;
341
345
if (from != null && Util .isSafeToRedirectTo (from )) {
342
346
redirectOnFinish = from ;
@@ -347,18 +351,19 @@ public HttpResponse doCommenceLogin(StaplerRequest request, @QueryParameter Stri
347
351
}
348
352
349
353
request .getSession ().setAttribute (REFERER_ATTRIBUTE , redirectOnFinish );
354
+ request .getSession ().setAttribute (STATE_ATTRIBUTE , state );
350
355
351
356
Set <String > scopes = new HashSet <>();
352
357
for (GitHubOAuthScope s : getJenkins ().getExtensionList (GitHubOAuthScope .class )) {
353
358
scopes .addAll (s .getScopesToRequest ());
354
359
}
355
360
String suffix ="" ;
356
361
if (!scopes .isEmpty ()) {
357
- suffix = "&scope=" +Util .join (scopes ,"," );
362
+ suffix = "&scope=" +Util .join (scopes ,"," )+ "&state=" + state ;
358
363
} else {
359
364
// We need repo scope in order to access private repos
360
365
// See https://developer.github.com/v3/oauth/#scopes
361
- suffix = "&scope=" + oauthScopes ;
366
+ suffix = "&scope=" + oauthScopes + "&state=" + state ;
362
367
}
363
368
364
369
return new HttpRedirect (githubWebUri + "/login/oauth/authorize?client_id="
@@ -372,13 +377,27 @@ public HttpResponse doCommenceLogin(StaplerRequest request, @QueryParameter Stri
372
377
public HttpResponse doFinishLogin (StaplerRequest request )
373
378
throws IOException {
374
379
String code = request .getParameter ("code" );
380
+ String state = request .getParameter (STATE_ATTRIBUTE );
375
381
String referer = (String )request .getSession ().getAttribute (REFERER_ATTRIBUTE );
382
+ String expectedState = (String )request .getSession ().getAttribute (STATE_ATTRIBUTE );
376
383
377
384
if (code == null || code .trim ().length () == 0 ) {
378
385
Log .info ("doFinishLogin: missing code." );
379
386
return HttpResponses .redirectToContextRoot ();
380
387
}
381
388
389
+ if (state == null ){
390
+ Log .info ("doFinishLogin: missing state parameter from Github response." );
391
+ return HttpResponses .redirectToContextRoot ();
392
+ } else if (expectedState == null ){
393
+ Log .info ("doFinishLogin: missing state parameter from user's session." );
394
+ return HttpResponses .redirectToContextRoot ();
395
+ } else if (!state .equals (expectedState )){
396
+ Log .info ("state parameter value [" +state +"] does not match the expected one [" +expectedState +"]" );
397
+ return HttpResponses .redirectToContextRoot ();
398
+ }
399
+
400
+
382
401
String accessToken = getAccessToken (code );
383
402
384
403
if (accessToken != null && accessToken .trim ().length () > 0 ) {
@@ -460,6 +479,21 @@ private String getAccessToken(@Nonnull String code) throws IOException {
460
479
return null ;
461
480
}
462
481
482
+ /**
483
+ * Generates a random URL Safe String of n characters
484
+ */
485
+ private String getSecureRandomString (int n ) {
486
+ if (n < 0 ){
487
+ throw new IllegalArgumentException ("Length must be a positive integer" );
488
+ }
489
+ // See RFC3986
490
+ final String urlSafeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_" ;
491
+ final StringBuilder sb = new StringBuilder ();
492
+ for (int i = 0 ; i < n ; i ++){
493
+ sb .append (urlSafeChars .charAt (SECURE_RANDOM .nextInt (urlSafeChars .length ())));
494
+ }
495
+ return sb .toString ();
496
+ }
463
497
/**
464
498
* Returns the proxy to be used when connecting to the given URI.
465
499
*/
@@ -659,7 +693,7 @@ public UserDetails loadUserByUsername(String username)
659
693
660
694
Authentication token = SecurityContextHolder .getContext ().getAuthentication ();
661
695
662
- if (token == null ) {
696
+ if (token == null || ! username . equals ( token . getPrincipal ()) ) {
663
697
if (localUser != null && GithubSecretStorage .contains (localUser )){
664
698
String accessToken = GithubSecretStorage .retrieve (localUser );
665
699
try {
@@ -789,6 +823,9 @@ static Jenkins getJenkins() {
789
823
private static final Logger LOGGER = Logger .getLogger (GithubSecurityRealm .class .getName ());
790
824
791
825
private static final String REFERER_ATTRIBUTE = GithubSecurityRealm .class .getName ()+".referer" ;
826
+ private static final String STATE_ATTRIBUTE = "state" ;
827
+
828
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom ();
792
829
793
830
/**
794
831
* Asks for the password.
0 commit comments