auth
auth/http.go
Bugfixes and tests for getting grants. Add tests for the grant confirmation part of the request to get a grant code. When the request is denied, the redirect should have an access_denied error. When the request is approved, the redirect should contain a code. Fix numerous bugs in which the redirect URL didn't contain the parameters we thought it would. Basically, anything that still used req.URL.Query().Set() instead of copying the query, modifying it, and setting req.URL.RawQuery. Fix a bug in which the redirectURL wasn't being properly set. Basically, when we moved the redirectURI processing to the top of the file (c29c7df35905 for those who forgot), we didn't update the reference to it lower in the file, where redirectURI was being updated and we expected that to be reflected in the processing. The HTTP handler for getting grant codes is now completely tested except for returning internal errors, which requires a new test harness be built to provoke internal errors on demand. At this point, however, I'd like to continue on implementing endpoints.
| 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@66 | 95 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore |
| paddy@66 | 96 redirectURI = u.String() |
| paddy@66 | 97 redirectURL = &u |
| paddy@56 | 98 } |
| paddy@56 | 99 } else { |
| paddy@56 | 100 validURI = false |
| paddy@56 | 101 } |
| paddy@56 | 102 if !validURI { |
| paddy@56 | 103 w.WriteHeader(http.StatusBadRequest) |
| paddy@56 | 104 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 105 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@56 | 106 }) |
| paddy@56 | 107 return |
| paddy@56 | 108 } |
| paddy@60 | 109 scope := r.URL.Query().Get("scope") |
| paddy@60 | 110 state := r.URL.Query().Get("state") |
| paddy@56 | 111 if r.URL.Query().Get("response_type") != "code" { |
| paddy@65 | 112 q := redirectURL.Query() |
| paddy@65 | 113 q.Add("error", "invalid_request") |
| paddy@65 | 114 q.Add("state", state) |
| paddy@65 | 115 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 116 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 117 return |
| paddy@56 | 118 } |
| paddy@56 | 119 if r.Method == "POST" { |
| paddy@63 | 120 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code. |
| paddy@56 | 121 if r.PostFormValue("grant") == "approved" { |
| paddy@60 | 122 code := uuid.NewID().String() |
| paddy@60 | 123 grant := Grant{ |
| paddy@60 | 124 Code: code, |
| paddy@60 | 125 Created: time.Now(), |
| paddy@60 | 126 ExpiresIn: defaultGrantExpiration, |
| paddy@60 | 127 ClientID: clientID, |
| paddy@60 | 128 Scope: scope, |
| paddy@60 | 129 RedirectURI: redirectURI, |
| paddy@60 | 130 State: state, |
| paddy@60 | 131 } |
| paddy@60 | 132 err := context.SaveGrant(grant) |
| paddy@60 | 133 if err != nil { |
| paddy@66 | 134 q := redirectURL.Query() |
| paddy@66 | 135 q.Add("error", "server_error") |
| paddy@66 | 136 q.Add("state", state) |
| paddy@66 | 137 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 138 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 139 return |
| paddy@60 | 140 } |
| paddy@66 | 141 q := redirectURL.Query() |
| paddy@66 | 142 q.Add("code", code) |
| paddy@66 | 143 q.Add("state", state) |
| paddy@66 | 144 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 145 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 146 return |
| paddy@56 | 147 } |
| paddy@66 | 148 q := redirectURL.Query() |
| paddy@66 | 149 q.Add("error", "access_denied") |
| paddy@66 | 150 q.Add("state", state) |
| paddy@66 | 151 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 152 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 153 return |
| paddy@56 | 154 } |
| paddy@51 | 155 w.WriteHeader(http.StatusOK) |
| paddy@56 | 156 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@56 | 157 "client": client, |
| paddy@56 | 158 }) |
| paddy@51 | 159 } |