auth
auth/http.go
Parse the redirect URI early, add new failure modes to tests. When obtaining a grant code, parse the redirect_uri early as a URL, so we can fail without even hitting the database, if possible. Add a test to cover the scenario where the client doesn't specify an endpoint, and the client has no endpoints registered. Add a test to cover the scenario where the client has two endpoints registered, but doesn't specify an endpoint. Fix the test that tested for invalid URLs by actually using an invalid URL. Apparently, "not a URL" is a valid URL. Go figure.
| paddy@51 | 1 package auth |
| paddy@51 | 2 |
| paddy@51 | 3 import ( |
| paddy@61 | 4 "html/template" |
| paddy@51 | 5 "net/http" |
| paddy@60 | 6 "net/url" |
| paddy@60 | 7 "time" |
| paddy@56 | 8 |
| paddy@56 | 9 "code.secondbit.org/uuid" |
| paddy@51 | 10 ) |
| paddy@51 | 11 |
| paddy@60 | 12 const ( |
| paddy@60 | 13 getGrantTemplateName = "get_grant" |
| paddy@60 | 14 defaultGrantExpiration = 600 // default to ten minute grant expirations |
| paddy@60 | 15 ) |
| paddy@51 | 16 |
| paddy@57 | 17 // GetGrantHandler presents and processes the page for asking a user to grant access |
| paddy@57 | 18 // to their data. See RFC 6749, Section 4.1. |
| paddy@51 | 19 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@56 | 20 if r.URL.Query().Get("client_id") == "" { |
| paddy@56 | 21 w.WriteHeader(http.StatusBadRequest) |
| paddy@56 | 22 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 23 "error": template.HTML("Client ID must be specified in the request."), |
| paddy@56 | 24 }) |
| paddy@56 | 25 return |
| paddy@56 | 26 } |
| paddy@56 | 27 clientID, err := uuid.Parse(r.URL.Query().Get("client_id")) |
| paddy@56 | 28 if err != nil { |
| paddy@56 | 29 w.WriteHeader(http.StatusBadRequest) |
| paddy@56 | 30 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 31 "error": template.HTML("client_id is not a valid Client ID."), |
| paddy@56 | 32 }) |
| paddy@56 | 33 return |
| paddy@56 | 34 } |
| paddy@64 | 35 redirectURI := r.URL.Query().Get("redirect_uri") |
| paddy@64 | 36 redirectURL, err := url.Parse(redirectURI) |
| paddy@64 | 37 if err != nil { |
| paddy@64 | 38 w.WriteHeader(http.StatusBadRequest) |
| paddy@64 | 39 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@64 | 40 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@64 | 41 }) |
| paddy@64 | 42 return |
| paddy@64 | 43 } |
| paddy@56 | 44 client, err := context.GetClient(clientID) |
| paddy@56 | 45 if err != nil { |
| paddy@59 | 46 if err == ErrClientNotFound { |
| paddy@59 | 47 w.WriteHeader(http.StatusBadRequest) |
| paddy@59 | 48 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 49 "error": template.HTML("The specified Client couldn’t be found."), |
| paddy@59 | 50 }) |
| paddy@59 | 51 } else { |
| paddy@59 | 52 w.WriteHeader(http.StatusInternalServerError) |
| paddy@59 | 53 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 54 "internal_error": template.HTML(err.Error()), |
| paddy@59 | 55 }) |
| paddy@59 | 56 } |
| paddy@56 | 57 return |
| paddy@56 | 58 } |
| paddy@56 | 59 // whether a redirect URI is valid or not depends on the number of endpoints |
| paddy@56 | 60 // the client has registered |
| paddy@56 | 61 numEndpoints, err := context.CountEndpoints(clientID) |
| paddy@56 | 62 if err != nil { |
| paddy@56 | 63 w.WriteHeader(http.StatusInternalServerError) |
| paddy@56 | 64 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 65 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 66 }) |
| paddy@56 | 67 return |
| paddy@56 | 68 } |
| paddy@56 | 69 var validURI bool |
| paddy@58 | 70 if redirectURI != "" { |
| paddy@58 | 71 // BUG(paddy): We really should normalize URIs before trying to compare them. |
| paddy@58 | 72 validURI, err = context.CheckEndpoint(clientID, redirectURI) |
| paddy@56 | 73 if err != nil { |
| paddy@56 | 74 w.WriteHeader(http.StatusInternalServerError) |
| paddy@56 | 75 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 76 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 77 }) |
| paddy@56 | 78 return |
| paddy@56 | 79 } |
| paddy@56 | 80 } else if redirectURI == "" && numEndpoints == 1 { |
| paddy@56 | 81 // if we don't specify the endpoint and there's only one endpoint, the |
| paddy@56 | 82 // request is valid, and we're redirecting to that one endpoint |
| paddy@56 | 83 validURI = true |
| paddy@56 | 84 endpoints, err := context.ListEndpoints(clientID, 1, 0) |
| paddy@56 | 85 if err != nil { |
| paddy@56 | 86 w.WriteHeader(http.StatusInternalServerError) |
| paddy@56 | 87 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 88 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 89 }) |
| paddy@56 | 90 return |
| paddy@56 | 91 } |
| paddy@56 | 92 if len(endpoints) != 1 { |
| paddy@56 | 93 validURI = false |
| paddy@56 | 94 } else { |
| paddy@56 | 95 redirectURI = endpoints[0].URI.String() |
| paddy@56 | 96 } |
| paddy@56 | 97 } else { |
| paddy@56 | 98 validURI = false |
| paddy@56 | 99 } |
| paddy@56 | 100 if !validURI { |
| paddy@56 | 101 w.WriteHeader(http.StatusBadRequest) |
| paddy@56 | 102 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 103 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@56 | 104 }) |
| paddy@56 | 105 return |
| paddy@56 | 106 } |
| paddy@60 | 107 scope := r.URL.Query().Get("scope") |
| paddy@60 | 108 state := r.URL.Query().Get("state") |
| paddy@56 | 109 if r.URL.Query().Get("response_type") != "code" { |
| paddy@60 | 110 redirectURL.Query().Add("error", "invalid_request") |
| paddy@60 | 111 redirectURL.Query().Add("state", state) |
| paddy@60 | 112 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 113 return |
| paddy@56 | 114 } |
| paddy@56 | 115 if r.Method == "POST" { |
| paddy@63 | 116 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code. |
| paddy@56 | 117 if r.PostFormValue("grant") == "approved" { |
| paddy@60 | 118 code := uuid.NewID().String() |
| paddy@60 | 119 grant := Grant{ |
| paddy@60 | 120 Code: code, |
| paddy@60 | 121 Created: time.Now(), |
| paddy@60 | 122 ExpiresIn: defaultGrantExpiration, |
| paddy@60 | 123 ClientID: clientID, |
| paddy@60 | 124 Scope: scope, |
| paddy@60 | 125 RedirectURI: redirectURI, |
| paddy@60 | 126 State: state, |
| paddy@60 | 127 } |
| paddy@60 | 128 err := context.SaveGrant(grant) |
| paddy@60 | 129 if err != nil { |
| paddy@60 | 130 redirectURL.Query().Add("error", "server_error") |
| paddy@60 | 131 redirectURL.Query().Add("state", state) |
| paddy@60 | 132 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 133 return |
| paddy@60 | 134 } |
| paddy@60 | 135 redirectURL.Query().Add("code", code) |
| paddy@60 | 136 redirectURL.Query().Add("state", state) |
| paddy@60 | 137 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 138 return |
| paddy@56 | 139 } |
| paddy@60 | 140 redirectURL.Query().Add("error", "access_denied") |
| paddy@60 | 141 redirectURL.Query().Add("state", state) |
| paddy@60 | 142 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 143 return |
| paddy@56 | 144 } |
| paddy@51 | 145 w.WriteHeader(http.StatusOK) |
| paddy@56 | 146 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@56 | 147 "client": client, |
| paddy@56 | 148 }) |
| paddy@51 | 149 } |