auth

Paddy 2015-03-03 Parent:d103a598548c Child:e090a69e711f

135:d30a3a12d387 Browse Files

Attach our Scope type to AuthCodes and Tokens. When obtaining an AuthorizationCode or Token, attach a slice of strings, each one a Scope ID, instead of just attaching the encoded string the user passes in. This will allow us to change our Scope encoding down the line, and is more conceptually faithful. Also, if an authorization request is made with an invalid scope, return the invalid_scope error.

authcode.go authcode_test.go client.go oauth2.go oauth2_test.go session.go token.go token_test.go

     1.1 --- a/authcode.go	Fri Feb 20 22:34:43 2015 -0500
     1.2 +++ b/authcode.go	Tue Mar 03 22:18:28 2015 -0500
     1.3 @@ -37,7 +37,7 @@
     1.4  	Created     time.Time
     1.5  	ExpiresIn   int32
     1.6  	ClientID    uuid.ID
     1.7 -	Scope       string
     1.8 +	Scopes      []string
     1.9  	RedirectURI string
    1.10  	State       string
    1.11  	ProfileID   uuid.ID
    1.12 @@ -95,7 +95,7 @@
    1.13  	return nil
    1.14  }
    1.15  
    1.16 -func authCodeGrantValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
    1.17 +func authCodeGrantValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
    1.18  	enc := json.NewEncoder(w)
    1.19  	code := r.PostFormValue("code")
    1.20  	if code == "" {
    1.21 @@ -129,7 +129,7 @@
    1.22  		renderJSONError(enc, "invalid_grant")
    1.23  		return
    1.24  	}
    1.25 -	return authCode.Scope, authCode.ProfileID, true
    1.26 +	return authCode.Scopes, authCode.ProfileID, true
    1.27  }
    1.28  
    1.29  func authCodeGrantInvalidate(r *http.Request, context Context) error {
     2.1 --- a/authcode_test.go	Fri Feb 20 22:34:43 2015 -0500
     2.2 +++ b/authcode_test.go	Tue Mar 03 22:18:28 2015 -0500
     2.3 @@ -28,8 +28,13 @@
     2.4  	if !authCode1.ClientID.Equal(authCode2.ClientID) {
     2.5  		return false, "client ID", authCode1.ClientID, authCode2.ClientID
     2.6  	}
     2.7 -	if authCode1.Scope != authCode2.Scope {
     2.8 -		return false, "scope", authCode1.Scope, authCode2.Scope
     2.9 +	if len(authCode1.Scopes) != len(authCode2.Scopes) {
    2.10 +		return false, "scopes", authCode1.Scopes, authCode2.Scopes
    2.11 +	}
    2.12 +	for pos, scope := range authCode1.Scopes {
    2.13 +		if scope != authCode2.Scopes[pos] {
    2.14 +			return false, "scopes", authCode1.Scopes, authCode2.Scopes
    2.15 +		}
    2.16  	}
    2.17  	if authCode1.RedirectURI != authCode2.RedirectURI {
    2.18  		return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
    2.19 @@ -53,7 +58,7 @@
    2.20  		Created:     time.Now(),
    2.21  		ExpiresIn:   180,
    2.22  		ClientID:    uuid.NewID(),
    2.23 -		Scope:       "scope",
    2.24 +		Scopes:      []string{"scope"},
    2.25  		RedirectURI: "redirectURI",
    2.26  		State:       "state",
    2.27  	}
    2.28 @@ -145,7 +150,7 @@
    2.29  		Created:     time.Now(),
    2.30  		ExpiresIn:   180,
    2.31  		ClientID:    uuid.NewID(),
    2.32 -		Scope:       "scope",
    2.33 +		Scopes:      []string{"scope"},
    2.34  		RedirectURI: "redirectURI",
    2.35  		State:       "state",
    2.36  	}
    2.37 @@ -327,7 +332,7 @@
    2.38  		Created:     time.Now(),
    2.39  		ExpiresIn:   180,
    2.40  		ClientID:    uuid.NewID(),
    2.41 -		Scope:       "scope",
    2.42 +		Scopes:      []string{"scope"},
    2.43  		RedirectURI: "redirectURI",
    2.44  		State:       "state",
    2.45  	}
     3.1 --- a/client.go	Fri Feb 20 22:34:43 2015 -0500
     3.2 +++ b/client.go	Tue Mar 03 22:18:28 2015 -0500
     3.3 @@ -9,6 +9,7 @@
     3.4  	"net/http"
     3.5  	"net/url"
     3.6  	"strconv"
     3.7 +	"strings"
     3.8  	"time"
     3.9  
    3.10  	"github.com/PuerkitoBio/purell"
    3.11 @@ -662,8 +663,8 @@
    3.12  	return
    3.13  }
    3.14  
    3.15 -func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
    3.16 -	scope = r.PostFormValue("scope")
    3.17 +func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
    3.18 +	scopes = strings.Split(r.PostFormValue("scope"), " ")
    3.19  	valid = true
    3.20  	return
    3.21  }
     4.1 --- a/oauth2.go	Fri Feb 20 22:34:43 2015 -0500
     4.2 +++ b/oauth2.go	Tue Mar 03 22:18:28 2015 -0500
     4.3 @@ -8,6 +8,7 @@
     4.4  	"net/http"
     4.5  	"net/url"
     4.6  	"strconv"
     4.7 +	"strings"
     4.8  	"sync"
     4.9  	"time"
    4.10  
    4.11 @@ -64,7 +65,7 @@
    4.12  // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
    4.13  // was successfully returned and the Invalidate function will be called asynchronously.
    4.14  type GrantType struct {
    4.15 -	Validate      func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
    4.16 +	Validate      func(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool)
    4.17  	Invalidate    func(r *http.Request, context Context) error
    4.18  	ReturnToken   func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
    4.19  	AuditString   func(r *http.Request) string
    4.20 @@ -264,7 +265,6 @@
    4.21  		})
    4.22  		return
    4.23  	}
    4.24 -	scope := r.URL.Query().Get("scope")
    4.25  	state := r.URL.Query().Get("state")
    4.26  	responseType := r.URL.Query().Get("response_type")
    4.27  	q := redirectURL.Query()
    4.28 @@ -275,6 +275,21 @@
    4.29  		http.Redirect(w, r, redirectURL.String(), http.StatusFound)
    4.30  		return
    4.31  	}
    4.32 +	scopeParams := strings.Split(r.URL.Query().Get("scope"), " ")
    4.33 +	scopes, err := context.GetScopes(scopeParams)
    4.34 +	if err != nil {
    4.35 +		if _, ok := err.(ErrScopeNotFound); ok {
    4.36 +			q.Add("error", "invalid_scope")
    4.37 +			redirectURL.RawQuery = q.Encode()
    4.38 +			http.Redirect(w, r, redirectURL.String(), http.StatusFound)
    4.39 +			return
    4.40 +		}
    4.41 +		log.Println("Error retrieving scopes:", err)
    4.42 +		q.Add("error", "server_error")
    4.43 +		redirectURL.RawQuery = q.Encode()
    4.44 +		http.Redirect(w, r, redirectURL.String(), http.StatusFound)
    4.45 +		return
    4.46 +	}
    4.47  	if r.Method == "POST" {
    4.48  		if checkCSRF(r, session) != nil {
    4.49  			log.Println("CSRF attempt detected.")
    4.50 @@ -294,7 +309,7 @@
    4.51  					Created:     time.Now(),
    4.52  					ExpiresIn:   defaultAuthorizationCodeExpiration,
    4.53  					ClientID:    clientID,
    4.54 -					Scope:       scope,
    4.55 +					Scopes:      scopeParams,
    4.56  					RedirectURI: r.URL.Query().Get("redirect_uri"),
    4.57  					State:       state,
    4.58  					ProfileID:   session.ProfileID,
    4.59 @@ -313,7 +328,7 @@
    4.60  					CreatedFrom: "implicit",
    4.61  					ExpiresIn:   defaultTokenExpiration,
    4.62  					TokenType:   "bearer",
    4.63 -					Scope:       scope,
    4.64 +					Scopes:      scopeParams,
    4.65  					ProfileID:   session.ProfileID,
    4.66  					ClientID:    clientID,
    4.67  				}
    4.68 @@ -327,7 +342,7 @@
    4.69  				q.Add("access_token", token.AccessToken)
    4.70  				q.Add("token_type", token.TokenType)
    4.71  				q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
    4.72 -				q.Add("scope", token.Scope)
    4.73 +				q.Add("scope", strings.Join(token.Scopes, " "))
    4.74  				q.Add("state", state) // we wiped out the old values, so we need to set the state again
    4.75  				fragment = true
    4.76  			}
    4.77 @@ -356,7 +371,7 @@
    4.78  	context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
    4.79  		"client":      client,
    4.80  		"redirectURL": redirectURL,
    4.81 -		"scope":       scope,
    4.82 +		"scopes":      scopes,
    4.83  		"profile":     profile,
    4.84  		"csrftoken":   session.CSRFToken,
    4.85  	})
    4.86 @@ -377,7 +392,7 @@
    4.87  	if !success {
    4.88  		return
    4.89  	}
    4.90 -	scope, profileID, valid := gt.Validate(w, r, context)
    4.91 +	scopes, profileID, valid := gt.Validate(w, r, context)
    4.92  	if !valid {
    4.93  		return
    4.94  	}
    4.95 @@ -392,7 +407,7 @@
    4.96  		CreatedFrom:  gt.AuditString(r),
    4.97  		ExpiresIn:    defaultTokenExpiration,
    4.98  		TokenType:    "bearer",
    4.99 -		Scope:        scope,
   4.100 +		Scopes:       scopes,
   4.101  		ProfileID:    profileID,
   4.102  		ClientID:     clientID,
   4.103  	}
     5.1 --- a/oauth2_test.go	Fri Feb 20 22:34:43 2015 -0500
     5.2 +++ b/oauth2_test.go	Tue Mar 03 22:18:28 2015 -0500
     5.3 @@ -15,8 +15,7 @@
     5.4  )
     5.5  
     5.6  const (
     5.7 -	scopeSet = 1 << iota
     5.8 -	stateSet
     5.9 +	stateSet = 1 << iota
    5.10  	uriSet
    5.11  )
    5.12  
    5.13 @@ -36,6 +35,7 @@
    5.14  		profiles:  store,
    5.15  		tokens:    store,
    5.16  		sessions:  store,
    5.17 +		scopes:    store,
    5.18  	}
    5.19  	client := Client{
    5.20  		ID:      uuid.NewID(),
    5.21 @@ -78,6 +78,15 @@
    5.22  	if err != nil {
    5.23  		t.Fatal("Can't store session:", err)
    5.24  	}
    5.25 +	scope := Scope{
    5.26 +		ID:          "testscope",
    5.27 +		Name:        "Test Scope",
    5.28 +		Description: "Hug dispensation.",
    5.29 +	}
    5.30 +	err = testContext.CreateScopes([]Scope{scope})
    5.31 +	if err != nil {
    5.32 +		t.Fatal("Can't store scope:", err)
    5.33 +	}
    5.34  	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
    5.35  	if err != nil {
    5.36  		t.Fatal("Can't build request:", err)
    5.37 @@ -87,18 +96,16 @@
    5.38  		Value: session.ID,
    5.39  	}
    5.40  	req.AddCookie(cookie)
    5.41 -	for i := 0; i < 1<<3; i++ {
    5.42 +	for i := 0; i < 1<<2; i++ {
    5.43  		w := httptest.NewRecorder()
    5.44  		params := url.Values{}
    5.45  		// see OAuth 2.0 spec, section 4.1.1
    5.46  		params.Set("response_type", "code")
    5.47  		params.Set("client_id", client.ID.String())
    5.48 +		params.Set("scope", "testscope")
    5.49  		if i&uriSet != 0 {
    5.50  			params.Set("redirect_uri", endpoint.URI)
    5.51  		}
    5.52 -		if i&scopeSet != 0 {
    5.53 -			params.Set("scope", "testscope")
    5.54 -		}
    5.55  		if i&stateSet != 0 {
    5.56  			params.Set("state", "my super secure state string")
    5.57  		}
    5.58 @@ -450,6 +457,7 @@
    5.59  		profiles:  store,
    5.60  		tokens:    store,
    5.61  		sessions:  store,
    5.62 +		scopes:    store,
    5.63  	}
    5.64  	client := Client{
    5.65  		ID:      uuid.NewID(),
    5.66 @@ -484,6 +492,15 @@
    5.67  	if err != nil {
    5.68  		t.Fatal("Can't store session:", err)
    5.69  	}
    5.70 +	scope := Scope{
    5.71 +		ID:          "testscope",
    5.72 +		Name:        "Test Scope",
    5.73 +		Description: "High five fabrication.",
    5.74 +	}
    5.75 +	err = testContext.CreateScopes([]Scope{scope})
    5.76 +	if err != nil {
    5.77 +		t.Fatal("Can't create scope:", err)
    5.78 +	}
    5.79  	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
    5.80  	if err != nil {
    5.81  		t.Fatal("Can't build request:", err)
    5.82 @@ -774,7 +791,7 @@
    5.83  		Created:     time.Now(),
    5.84  		ExpiresIn:   600,
    5.85  		ClientID:    client.ID,
    5.86 -		Scope:       "testscope",
    5.87 +		Scopes:      []string{"testscope"},
    5.88  		RedirectURI: "https://client.secondbit.org/",
    5.89  		State:       "teststate",
    5.90  		ProfileID:   uuid.NewID(),
     6.1 --- a/session.go	Fri Feb 20 22:34:43 2015 -0500
     6.2 +++ b/session.go	Tue Mar 03 22:18:28 2015 -0500
     6.3 @@ -10,6 +10,7 @@
     6.4  	"log"
     6.5  	"net/http"
     6.6  	"sort"
     6.7 +	"strings"
     6.8  	"time"
     6.9  
    6.10  	"code.secondbit.org/pass.hg"
    6.11 @@ -319,11 +320,11 @@
    6.12  	})
    6.13  }
    6.14  
    6.15 -func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
    6.16 +func credentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
    6.17  	enc := json.NewEncoder(w)
    6.18  	username := r.PostFormValue("username")
    6.19  	password := r.PostFormValue("password")
    6.20 -	scope = r.PostFormValue("scope")
    6.21 +	scopes = strings.Split(r.PostFormValue("scope"), " ")
    6.22  	profile, err := authenticate(username, password, context)
    6.23  	if err != nil {
    6.24  		if err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked {
     7.1 --- a/token.go	Fri Feb 20 22:34:43 2015 -0500
     7.2 +++ b/token.go	Tue Mar 03 22:18:28 2015 -0500
     7.3 @@ -43,7 +43,7 @@
     7.4  	CreatedFrom    string
     7.5  	ExpiresIn      int32
     7.6  	TokenType      string
     7.7 -	Scope          string
     7.8 +	Scopes         []string
     7.9  	ProfileID      uuid.ID
    7.10  	ClientID       uuid.ID
    7.11  	Revoked        bool
    7.12 @@ -139,7 +139,7 @@
    7.13  	return tokens, nil
    7.14  }
    7.15  
    7.16 -func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
    7.17 +func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
    7.18  	enc := json.NewEncoder(w)
    7.19  	refresh := r.PostFormValue("refresh_token")
    7.20  	if refresh == "" {
    7.21 @@ -173,7 +173,7 @@
    7.22  		renderJSONError(enc, "invalid_grant")
    7.23  		return
    7.24  	}
    7.25 -	return token.Scope, token.ProfileID, true
    7.26 +	return token.Scopes, token.ProfileID, true
    7.27  }
    7.28  
    7.29  func refreshTokenInvalidate(r *http.Request, context Context) error {
     8.1 --- a/token_test.go	Fri Feb 20 22:34:43 2015 -0500
     8.2 +++ b/token_test.go	Tue Mar 03 22:18:28 2015 -0500
     8.3 @@ -28,8 +28,13 @@
     8.4  	if token1.TokenType != token2.TokenType {
     8.5  		return false, "token type", token1.TokenType, token2.TokenType
     8.6  	}
     8.7 -	if token1.Scope != token2.Scope {
     8.8 -		return false, "scope", token1.Scope, token2.Scope
     8.9 +	if len(token1.Scopes) != len(token2.Scopes) {
    8.10 +		return false, "scopes", token1.Scopes, token2.Scopes
    8.11 +	}
    8.12 +	for pos, scope := range token1.Scopes {
    8.13 +		if scope != token2.Scopes[pos] {
    8.14 +			return false, "scopes", token1.Scopes, token2.Scopes
    8.15 +		}
    8.16  	}
    8.17  	if !token1.ProfileID.Equal(token2.ProfileID) {
    8.18  		return false, "profile ID", token1.ProfileID, token2.ProfileID
    8.19 @@ -48,7 +53,7 @@
    8.20  		Created:      time.Now(),
    8.21  		ExpiresIn:    3600,
    8.22  		TokenType:    "bearer",
    8.23 -		Scope:        "scope",
    8.24 +		Scopes:       []string{"scope"},
    8.25  		ProfileID:    uuid.NewID(),
    8.26  	}
    8.27  	for _, store := range tokenStores {