auth
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.
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 +}