auth

Paddy 2014-10-22 Parent:cfab12566289 Child:e45bfa2abc00

56:a5987795707e Browse Files

Actually validate grant requests. Write the logic to validate grant requests and stub out the rendering/error handling/redirecting locations. Finally, we get to the good stuff: implementing the specification. Write some tests to verify that granting requests works the way we think it does.

http.go http_test.go

     1.1 --- a/http.go	Wed Oct 22 00:29:05 2014 -0400
     1.2 +++ b/http.go	Wed Oct 22 00:30:28 2014 -0400
     1.3 @@ -1,16 +1,113 @@
     1.4  package auth
     1.5  
     1.6  import (
     1.7 -	"log"
     1.8  	"net/http"
     1.9 +
    1.10 +	"code.secondbit.org/uuid"
    1.11  )
    1.12  
    1.13  const getGrantTemplateName = "get_grant"
    1.14  
    1.15  func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
    1.16 +	if r.URL.Query().Get("client_id") == "" {
    1.17 +		w.WriteHeader(http.StatusBadRequest)
    1.18 +		context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.19 +			"error": "Client ID must be specified in the request.",
    1.20 +		})
    1.21 +		return
    1.22 +	}
    1.23 +	clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
    1.24 +	if err != nil {
    1.25 +		w.WriteHeader(http.StatusBadRequest)
    1.26 +		context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.27 +			"error": "client_id is not a valid Client ID.",
    1.28 +		})
    1.29 +		return
    1.30 +	}
    1.31 +	client, err := context.GetClient(clientID)
    1.32 +	if err != nil {
    1.33 +		w.WriteHeader(http.StatusInternalServerError)
    1.34 +		context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.35 +			"internal_error": err,
    1.36 +		})
    1.37 +		return
    1.38 +	}
    1.39 +	// whether a redirect URI is valid or not depends on the number of endpoints
    1.40 +	// the client has registered
    1.41 +	numEndpoints, err := context.CountEndpoints(clientID)
    1.42 +	if err != nil {
    1.43 +		w.WriteHeader(http.StatusInternalServerError)
    1.44 +		context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.45 +			"internal_error": err,
    1.46 +		})
    1.47 +		return
    1.48 +	}
    1.49 +	redirectURI := r.URL.Query().Get("redirect_uri")
    1.50 +	var validURI bool
    1.51 +	if redirectURI != "" && numEndpoints > 1 {
    1.52 +		// if there's more than one registered endpoint, we need to match the
    1.53 +		// entire thing, character for character. So use strict checking.
    1.54 +		validURI, err = context.CheckEndpoint(clientID, redirectURI, true)
    1.55 +		if err != nil {
    1.56 +			w.WriteHeader(http.StatusInternalServerError)
    1.57 +			context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.58 +				"internal_error": err,
    1.59 +			})
    1.60 +			return
    1.61 +		}
    1.62 +	} else if redirectURI != "" && numEndpoints == 1 {
    1.63 +		// if there's exactly one endpoint, we can match only the prefix of it,
    1.64 +		// so don't use strict checking.
    1.65 +		validURI, err = context.CheckEndpoint(clientID, redirectURI, false)
    1.66 +		if err != nil {
    1.67 +			w.WriteHeader(http.StatusInternalServerError)
    1.68 +			context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.69 +				"internal_error": err,
    1.70 +			})
    1.71 +			return
    1.72 +		}
    1.73 +	} else if redirectURI == "" && numEndpoints == 1 {
    1.74 +		// if we don't specify the endpoint and there's only one endpoint, the
    1.75 +		// request is valid, and we're redirecting to that one endpoint
    1.76 +		validURI = true
    1.77 +		endpoints, err := context.ListEndpoints(clientID, 1, 0)
    1.78 +		if err != nil {
    1.79 +			w.WriteHeader(http.StatusInternalServerError)
    1.80 +			context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.81 +				"internal_error": err,
    1.82 +			})
    1.83 +			return
    1.84 +		}
    1.85 +		if len(endpoints) != 1 {
    1.86 +			validURI = false
    1.87 +		} else {
    1.88 +			redirectURI = endpoints[0].URI.String()
    1.89 +		}
    1.90 +	} else {
    1.91 +		validURI = false
    1.92 +	}
    1.93 +	if !validURI {
    1.94 +		w.WriteHeader(http.StatusBadRequest)
    1.95 +		context.Render(w, getGrantTemplateName, map[string]interface{}{
    1.96 +			"error": "The redirect_uri specified is not valid.",
    1.97 +		})
    1.98 +		return
    1.99 +	}
   1.100 +	if r.URL.Query().Get("response_type") != "code" {
   1.101 +		// TODO: redirect error
   1.102 +	}
   1.103 +	//scope := r.URL.Query().Get("scope")
   1.104 +	//state := r.URL.Query().Get("state")
   1.105 +	if r.Method == "POST" {
   1.106 +		// TODO: CSRF protection
   1.107 +		if r.PostFormValue("grant") == "approved" {
   1.108 +			// TODO: redirect
   1.109 +		} else {
   1.110 +			// TODO: redirect error
   1.111 +		}
   1.112 +	}
   1.113  	w.WriteHeader(http.StatusOK)
   1.114 -	err := context.Render(w, getGrantTemplateName, nil)
   1.115 -	if err != nil {
   1.116 -		log.Println("Error rendering template for GetGrantHandler:", err)
   1.117 -	}
   1.118 +	context.Render(w, getGrantTemplateName, map[string]interface{}{
   1.119 +		"client": client,
   1.120 +	})
   1.121  }
     2.1 --- a/http_test.go	Wed Oct 22 00:29:05 2014 -0400
     2.2 +++ b/http_test.go	Wed Oct 22 00:30:28 2014 -0400
     2.3 @@ -6,12 +6,16 @@
     2.4  	"net/http/httptest"
     2.5  	"net/url"
     2.6  	"testing"
     2.7 +	"time"
     2.8 +
     2.9 +	"code.secondbit.org/uuid"
    2.10  )
    2.11  
    2.12  const (
    2.13  	scopeSet = 1 << iota
    2.14  	stateSet
    2.15  	uriSet
    2.16 +	uriExact
    2.17  )
    2.18  
    2.19  func TestGetGrantCodeSuccess(t *testing.T) {
    2.20 @@ -24,18 +28,49 @@
    2.21  		profiles: store,
    2.22  		tokens:   store,
    2.23  	}
    2.24 +	client := Client{
    2.25 +		ID:      uuid.NewID(),
    2.26 +		Secret:  "super secret!",
    2.27 +		OwnerID: uuid.NewID(),
    2.28 +		Name:    "My test client",
    2.29 +		Logo:    "https://secondbit.org/logo.png",
    2.30 +		Website: "https://secondbit.org",
    2.31 +		Type:    "public",
    2.32 +	}
    2.33 +	uri, err := url.Parse("https://test.secondbit.org/redirect")
    2.34 +	if err != nil {
    2.35 +		t.Fatal("Can't parse URL:", err)
    2.36 +	}
    2.37 +	endpoint := Endpoint{
    2.38 +		ID:       uuid.NewID(),
    2.39 +		ClientID: client.ID,
    2.40 +		URI:      *uri,
    2.41 +		Added:    time.Now(),
    2.42 +	}
    2.43 +	err = testContext.SaveClient(client)
    2.44 +	if err != nil {
    2.45 +		t.Fatal("Can't store client:", err)
    2.46 +	}
    2.47 +	err = testContext.AddEndpoint(client.ID, endpoint)
    2.48 +	if err != nil {
    2.49 +		t.Fatal("Can't store endpoint:", err)
    2.50 +	}
    2.51  	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
    2.52  	if err != nil {
    2.53  		t.Fatal("Can't build request:", err)
    2.54  	}
    2.55 -	for i := 0; i < 1<<3; i++ {
    2.56 +	for i := 0; i < 1<<4; i++ {
    2.57  		w := httptest.NewRecorder()
    2.58  		params := url.Values{}
    2.59  		// see OAuth 2.0 spec, section 4.1.1
    2.60  		params.Set("response_type", "code")
    2.61 -		params.Set("client_id", "test_client_id")
    2.62 +		params.Set("client_id", client.ID.String())
    2.63  		if i&uriSet != 0 {
    2.64 -			params.Set("redirect_uri", "https://test.secondbit.org/redirect")
    2.65 +			if i&uriExact != 0 {
    2.66 +				params.Set("redirect_uri", endpoint.URI.String())
    2.67 +			} else {
    2.68 +				params.Set("redirect_uri", endpoint.URI.String()+"/inexact")
    2.69 +			}
    2.70  		}
    2.71  		if i&scopeSet != 0 {
    2.72  			params.Set("scope", "testscope")
    2.73 @@ -53,3 +88,57 @@
    2.74  		}
    2.75  	}
    2.76  }
    2.77 +
    2.78 +func TestGetGrantCodeInvalidURI(t *testing.T) {
    2.79 +	t.Parallel()
    2.80 +	store := NewMemstore()
    2.81 +	testContext := Context{
    2.82 +		template: template.Must(template.New(getGrantTemplateName).Parse("{{ .error }}")),
    2.83 +		clients:  store,
    2.84 +		grants:   store,
    2.85 +		profiles: store,
    2.86 +		tokens:   store,
    2.87 +	}
    2.88 +	client := Client{
    2.89 +		ID:      uuid.NewID(),
    2.90 +		Secret:  "super secret!",
    2.91 +		OwnerID: uuid.NewID(),
    2.92 +		Name:    "My test client",
    2.93 +		Type:    "public",
    2.94 +	}
    2.95 +	uri, err := url.Parse("https://test.secondbit.org/redirect")
    2.96 +	if err != nil {
    2.97 +		t.Fatal("Can't parse URL:", err)
    2.98 +	}
    2.99 +	endpoint := Endpoint{
   2.100 +		ID:       uuid.NewID(),
   2.101 +		ClientID: client.ID,
   2.102 +		URI:      *uri,
   2.103 +		Added:    time.Now(),
   2.104 +	}
   2.105 +	err = testContext.SaveClient(client)
   2.106 +	if err != nil {
   2.107 +		t.Fatal("Can't store client:", err)
   2.108 +	}
   2.109 +	err = testContext.AddEndpoint(client.ID, endpoint)
   2.110 +	if err != nil {
   2.111 +		t.Fatal("Can't store endpoint:", err)
   2.112 +	}
   2.113 +	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.114 +	if err != nil {
   2.115 +		t.Fatal("Can't build request:", err)
   2.116 +	}
   2.117 +	w := httptest.NewRecorder()
   2.118 +	params := url.Values{}
   2.119 +	params.Set("response_type", "code")
   2.120 +	params.Set("client_id", client.ID.String())
   2.121 +	params.Set("redirect_uri", "https://test.secondbit.org/wrong")
   2.122 +	req.URL.RawQuery = params.Encode()
   2.123 +	GetGrantHandler(w, req, testContext)
   2.124 +	if w.Code != http.StatusBadRequest {
   2.125 +		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
   2.126 +	}
   2.127 +	if w.Body.String() != "The redirect_uri specified is not valid." {
   2.128 +		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
   2.129 +	}
   2.130 +}