auth
auth/access.go
Consistently handle context in client storage interface. Let's at least be consistent about passing or not passing context to the client storage interface. Most our methods don't take the context, so let's just remove it.
| paddy@6 | 1 package auth |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "net/http" |
| paddy@1 | 5 "net/url" |
| paddy@0 | 6 "time" |
| paddy@0 | 7 |
| paddy@1 | 8 "strconv" |
| paddy@0 | 9 "secondbit.org/uuid" |
| paddy@0 | 10 ) |
| paddy@0 | 11 |
| paddy@0 | 12 // GrantType is the type for OAuth param `grant_type` |
| paddy@0 | 13 type GrantType string |
| paddy@0 | 14 |
| paddy@0 | 15 const ( |
| paddy@0 | 16 AuthorizationCodeGrant GrantType = "authorization_code" |
| paddy@0 | 17 RefreshTokenGrant = "refresh_token" |
| paddy@0 | 18 PasswordGrant = "password" |
| paddy@0 | 19 ClientCredentialsGrant = "client_credentials" |
| paddy@0 | 20 ) |
| paddy@0 | 21 |
| paddy@0 | 22 // AccessData represents an access grant (tokens, expiration, client, etc) |
| paddy@0 | 23 type AccessData struct { |
| paddy@1 | 24 PreviousAuthorizeData *AuthorizeData |
| paddy@1 | 25 PreviousAccessData *AccessData // previous access data, when refreshing |
| paddy@1 | 26 AccessToken string |
| paddy@1 | 27 RefreshToken string |
| paddy@1 | 28 ExpiresIn int32 |
| paddy@1 | 29 CreatedAt time.Time |
| paddy@1 | 30 TokenType string |
| paddy@1 | 31 ProfileID uuid.ID |
| paddy@1 | 32 AuthRequest |
| paddy@0 | 33 } |
| paddy@0 | 34 |
| paddy@0 | 35 // IsExpired returns true if access expired |
| paddy@0 | 36 func (d *AccessData) IsExpired() bool { |
| paddy@0 | 37 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now()) |
| paddy@0 | 38 } |
| paddy@0 | 39 |
| paddy@0 | 40 // ExpireAt returns the expiration date |
| paddy@0 | 41 func (d *AccessData) ExpireAt() time.Time { |
| paddy@0 | 42 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second) |
| paddy@0 | 43 } |
| paddy@0 | 44 |
| paddy@0 | 45 // HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests. |
| paddy@0 | 46 func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 47 // Only allow GET or POST |
| paddy@0 | 48 if r.Method != "POST" { |
| paddy@1 | 49 if r.Method != "GET" || !ctx.Config.AllowGetAccessRequest { |
| paddy@3 | 50 ctx.RenderJSONError(w, ErrorInvalidRequest, "Invalid request method.", ctx.Config.DocumentationDomain) |
| paddy@0 | 51 return |
| paddy@0 | 52 } |
| paddy@0 | 53 } |
| paddy@0 | 54 |
| paddy@0 | 55 grantType := GrantType(r.Form.Get("grant_type")) |
| paddy@0 | 56 if ctx.Config.AllowedAccessTypes.Exists(grantType) { |
| paddy@0 | 57 switch grantType { |
| paddy@0 | 58 case AuthorizationCodeGrant: |
| paddy@0 | 59 handleAuthorizationCodeRequest(w, r, ctx) |
| paddy@0 | 60 case RefreshTokenGrant: |
| paddy@0 | 61 handleRefreshTokenRequest(w, r, ctx) |
| paddy@0 | 62 case PasswordGrant: |
| paddy@0 | 63 handlePasswordRequest(w, r, ctx) |
| paddy@0 | 64 case ClientCredentialsGrant: |
| paddy@0 | 65 handleClientCredentialsRequest(w, r, ctx) |
| paddy@0 | 66 default: |
| paddy@3 | 67 ctx.RenderJSONError(w, ErrorUnsupportedGrantType, "Unsupported grant type.", ctx.Config.DocumentationDomain) |
| paddy@0 | 68 return |
| paddy@0 | 69 } |
| paddy@0 | 70 } |
| paddy@0 | 71 } |
| paddy@0 | 72 |
| paddy@0 | 73 func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 74 // get client authentication |
| paddy@0 | 75 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 76 if err != nil { |
| paddy@3 | 77 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 78 return |
| paddy@0 | 79 } |
| paddy@0 | 80 |
| paddy@1 | 81 code := r.Form.Get("code") |
| paddy@0 | 82 // "code" is required |
| paddy@1 | 83 if code == "" { |
| paddy@3 | 84 ctx.RenderJSONError(w, ErrorInvalidRequest, "Code must be supplied.", ctx.Config.DocumentationDomain) |
| paddy@0 | 85 return |
| paddy@0 | 86 } |
| paddy@0 | 87 |
| paddy@0 | 88 // must have a valid client |
| paddy@1 | 89 client, err := getClient(auth, ctx) |
| paddy@0 | 90 if err != nil { |
| paddy@5 | 91 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 92 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 93 return |
| paddy@5 | 94 } |
| paddy@9 | 95 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 96 return |
| paddy@0 | 97 } |
| paddy@0 | 98 |
| paddy@0 | 99 // must be a valid authorization code |
| paddy@1 | 100 authData, err := ctx.Tokens.GetAuthorization(code) |
| paddy@0 | 101 if err != nil { |
| paddy@9 | 102 if err == AuthorizationNotFoundError { |
| paddy@9 | 103 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid authorization.", ctx.Config.DocumentationDomain) |
| paddy@9 | 104 return |
| paddy@9 | 105 } |
| paddy@9 | 106 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 107 return |
| paddy@0 | 108 } |
| paddy@5 | 109 if authData.RedirectURI == "" { |
| paddy@5 | 110 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain) |
| paddy@0 | 111 return |
| paddy@5 | 112 } |
| paddy@1 | 113 if authData.IsExpired() { |
| paddy@3 | 114 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain) |
| paddy@3 | 115 return |
| paddy@0 | 116 } |
| paddy@0 | 117 |
| paddy@0 | 118 // code must be from the client |
| paddy@1 | 119 if !authData.Client.ID.Equal(client.ID) { |
| paddy@3 | 120 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 121 return |
| paddy@0 | 122 } |
| paddy@0 | 123 |
| paddy@0 | 124 // check redirect uri |
| paddy@1 | 125 redirectURI := r.Form.Get("redirect_uri") |
| paddy@1 | 126 if redirectURI == "" { |
| paddy@1 | 127 redirectURI = client.RedirectURI |
| paddy@0 | 128 } |
| paddy@1 | 129 if err = validateURI(client.RedirectURI, redirectURI); err != nil { |
| paddy@3 | 130 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 131 return |
| paddy@0 | 132 } |
| paddy@1 | 133 if authData.RedirectURI != redirectURI { |
| paddy@3 | 134 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain) |
| paddy@0 | 135 return |
| paddy@0 | 136 } |
| paddy@0 | 137 |
| paddy@1 | 138 data := AccessData{ |
| paddy@1 | 139 AuthRequest: AuthRequest{ |
| paddy@1 | 140 Client: client, |
| paddy@1 | 141 RedirectURI: redirectURI, |
| paddy@1 | 142 Scope: authData.Scope, |
| paddy@1 | 143 }, |
| paddy@1 | 144 PreviousAuthorizeData: &authData, |
| paddy@1 | 145 } |
| paddy@1 | 146 |
| paddy@1 | 147 err = fillTokens(&data, true, ctx) |
| paddy@1 | 148 if err != nil { |
| paddy@7 | 149 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 150 return |
| paddy@1 | 151 } |
| paddy@4 | 152 ctx.RenderJSONToken(w, data) |
| paddy@0 | 153 } |
| paddy@0 | 154 |
| paddy@0 | 155 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 156 // get client authentication |
| paddy@0 | 157 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@3 | 158 |
| paddy@0 | 159 if err != nil { |
| paddy@3 | 160 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 161 return |
| paddy@0 | 162 } |
| paddy@0 | 163 |
| paddy@1 | 164 code := r.Form.Get("refresh_token") |
| paddy@0 | 165 |
| paddy@0 | 166 // "refresh_token" is required |
| paddy@1 | 167 if code == "" { |
| paddy@3 | 168 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain) |
| paddy@0 | 169 return |
| paddy@0 | 170 } |
| paddy@0 | 171 |
| paddy@0 | 172 // must have a valid client |
| paddy@1 | 173 client, err := getClient(auth, ctx) |
| paddy@0 | 174 if err != nil { |
| paddy@5 | 175 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 176 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 177 return |
| paddy@5 | 178 } |
| paddy@9 | 179 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 180 return |
| paddy@0 | 181 } |
| paddy@0 | 182 |
| paddy@0 | 183 // must be a valid refresh code |
| paddy@1 | 184 refreshData, err := ctx.Tokens.GetRefresh(code) |
| paddy@0 | 185 if err != nil { |
| paddy@9 | 186 if err == TokenNotFoundError { |
| paddy@9 | 187 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token not valid.", ctx.Config.DocumentationDomain) |
| paddy@9 | 188 return |
| paddy@9 | 189 } |
| paddy@9 | 190 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 191 return |
| paddy@0 | 192 } |
| paddy@0 | 193 |
| paddy@0 | 194 // client must be the same as the previous token |
| paddy@1 | 195 if !refreshData.Client.ID.Equal(client.ID) { |
| paddy@3 | 196 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain) |
| paddy@0 | 197 return |
| paddy@0 | 198 } |
| paddy@0 | 199 |
| paddy@1 | 200 scope := r.Form.Get("scope") |
| paddy@1 | 201 if scope == "" { |
| paddy@1 | 202 scope = refreshData.Scope |
| paddy@0 | 203 } |
| paddy@0 | 204 |
| paddy@1 | 205 data := AccessData{ |
| paddy@1 | 206 AuthRequest: AuthRequest{ |
| paddy@5 | 207 Client: client, |
| paddy@5 | 208 Scope: scope, |
| paddy@1 | 209 }, |
| paddy@1 | 210 PreviousAccessData: &refreshData, |
| paddy@1 | 211 } |
| paddy@1 | 212 err = fillTokens(&data, true, ctx) |
| paddy@1 | 213 if err != nil { |
| paddy@7 | 214 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 215 return |
| paddy@1 | 216 } |
| paddy@4 | 217 ctx.RenderJSONToken(w, data) |
| paddy@0 | 218 } |
| paddy@0 | 219 |
| paddy@0 | 220 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 221 // get client authentication |
| paddy@0 | 222 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 223 if err != nil { |
| paddy@4 | 224 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 225 return |
| paddy@0 | 226 } |
| paddy@0 | 227 |
| paddy@1 | 228 username := r.Form.Get("username") |
| paddy@1 | 229 password := r.Form.Get("password") |
| paddy@1 | 230 scope := r.Form.Get("scope") |
| paddy@0 | 231 |
| paddy@0 | 232 // "username" and "password" is required |
| paddy@1 | 233 if username == "" || password == "" { |
| paddy@4 | 234 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain) |
| paddy@0 | 235 return |
| paddy@0 | 236 } |
| paddy@0 | 237 |
| paddy@0 | 238 // must have a valid client |
| paddy@1 | 239 client, err := getClient(auth, ctx) |
| paddy@0 | 240 if err != nil { |
| paddy@5 | 241 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 242 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 243 return |
| paddy@5 | 244 } |
| paddy@9 | 245 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 246 return |
| paddy@0 | 247 } |
| paddy@0 | 248 |
| paddy@1 | 249 _, err = ctx.Profiles.GetProfile(username, password) |
| paddy@1 | 250 if err != nil { |
| paddy@9 | 251 if err == ProfileNotFoundError { |
| paddy@9 | 252 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid credentials.", ctx.Config.DocumentationDomain) |
| paddy@9 | 253 return |
| paddy@9 | 254 } |
| paddy@9 | 255 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 256 return |
| paddy@1 | 257 } |
| paddy@1 | 258 |
| paddy@1 | 259 data := AccessData{ |
| paddy@1 | 260 AuthRequest: AuthRequest{ |
| paddy@5 | 261 Client: client, |
| paddy@5 | 262 Scope: scope, |
| paddy@1 | 263 }, |
| paddy@1 | 264 } |
| paddy@1 | 265 |
| paddy@1 | 266 err = fillTokens(&data, true, ctx) |
| paddy@1 | 267 if err != nil { |
| paddy@7 | 268 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@1 | 269 return |
| paddy@1 | 270 } |
| paddy@4 | 271 ctx.RenderJSONToken(w, data) |
| paddy@0 | 272 } |
| paddy@0 | 273 |
| paddy@0 | 274 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) { |
| paddy@0 | 275 // get client authentication |
| paddy@0 | 276 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams) |
| paddy@0 | 277 if err != nil { |
| paddy@4 | 278 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@0 | 279 return |
| paddy@0 | 280 } |
| paddy@0 | 281 |
| paddy@1 | 282 scope := r.Form.Get("scope") |
| paddy@0 | 283 |
| paddy@0 | 284 // must have a valid client |
| paddy@1 | 285 client, err := getClient(auth, ctx) |
| paddy@0 | 286 if err != nil { |
| paddy@5 | 287 if err == ClientNotFoundError || err == InvalidClientError { |
| paddy@5 | 288 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain) |
| paddy@9 | 289 return |
| paddy@5 | 290 } |
| paddy@9 | 291 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 292 return |
| paddy@0 | 293 } |
| paddy@0 | 294 |
| paddy@1 | 295 data := AccessData{ |
| paddy@1 | 296 AuthRequest: AuthRequest{ |
| paddy@5 | 297 Client: client, |
| paddy@5 | 298 Scope: scope, |
| paddy@1 | 299 }, |
| paddy@1 | 300 } |
| paddy@0 | 301 |
| paddy@1 | 302 err = fillTokens(&data, true, ctx) |
| paddy@0 | 303 if err != nil { |
| paddy@7 | 304 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain) |
| paddy@0 | 305 return |
| paddy@0 | 306 } |
| paddy@4 | 307 ctx.RenderJSONToken(w, data) |
| paddy@0 | 308 } |
| paddy@0 | 309 |
| paddy@1 | 310 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error { |
| paddy@0 | 311 var err error |
| paddy@0 | 312 |
| paddy@0 | 313 // generate access token |
| paddy@1 | 314 data.AccessToken = newToken() |
| paddy@1 | 315 if includeRefresh { |
| paddy@1 | 316 data.RefreshToken = newToken() |
| paddy@0 | 317 } |
| paddy@0 | 318 |
| paddy@0 | 319 // save access token |
| paddy@1 | 320 err = ctx.Tokens.SaveAccess(*data) |
| paddy@0 | 321 if err != nil { |
| paddy@10 | 322 if ctx.Log != nil { |
| paddy@10 | 323 ctx.Log.Printf("Error writing access token: %s\n", err) |
| paddy@10 | 324 } |
| paddy@7 | 325 return InternalServerError |
| paddy@0 | 326 } |
| paddy@0 | 327 |
| paddy@0 | 328 // remove authorization token |
| paddy@1 | 329 if data.PreviousAuthorizeData != nil { |
| paddy@1 | 330 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code) |
| paddy@10 | 331 if err != nil && ctx.Log != nil { |
| paddy@10 | 332 ctx.Log.Printf("Error removing previous auth data (%s): %s\n", data.PreviousAuthorizeData.Code, err) |
| paddy@0 | 333 } |
| paddy@0 | 334 } |
| paddy@0 | 335 |
| paddy@0 | 336 // remove previous access token |
| paddy@1 | 337 if data.PreviousAccessData != nil { |
| paddy@1 | 338 if data.PreviousAccessData.RefreshToken != "" { |
| paddy@1 | 339 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken) |
| paddy@10 | 340 if err != nil && ctx.Log != nil { |
| paddy@10 | 341 ctx.Log.Printf("Error removing previous refresh token (%s): %s\n", data.PreviousAccessData.RefreshToken, err) |
| paddy@0 | 342 } |
| paddy@0 | 343 } |
| paddy@1 | 344 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken) |
| paddy@10 | 345 if err != nil && ctx.Log != nil { |
| paddy@10 | 346 ctx.Log.Printf("Error removing previous access token (%s): %s\n", data.PreviousAccessData.AccessToken, err) |
| paddy@0 | 347 } |
| paddy@0 | 348 } |
| paddy@0 | 349 |
| paddy@1 | 350 data.TokenType = ctx.Config.TokenType |
| paddy@1 | 351 data.ExpiresIn = ctx.Config.AccessExpiration |
| paddy@1 | 352 data.CreatedAt = time.Now() |
| paddy@1 | 353 return nil |
| paddy@0 | 354 } |
| paddy@0 | 355 |
| paddy@1 | 356 func (data AccessData) GetRedirect(fragment bool) (string, error) { |
| paddy@1 | 357 u, err := url.Parse(data.RedirectURI) |
| paddy@1 | 358 if err != nil { |
| paddy@1 | 359 return "", err |
| paddy@1 | 360 } |
| paddy@1 | 361 |
| paddy@1 | 362 // add parameters |
| paddy@1 | 363 q := u.Query() |
| paddy@1 | 364 q.Set("access_token", data.AccessToken) |
| paddy@1 | 365 q.Set("token_type", data.TokenType) |
| paddy@1 | 366 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10)) |
| paddy@1 | 367 if data.RefreshToken != "" { |
| paddy@1 | 368 q.Set("refresh_token", data.RefreshToken) |
| paddy@1 | 369 } |
| paddy@1 | 370 if data.Scope != "" { |
| paddy@1 | 371 q.Set("scope", data.Scope) |
| paddy@1 | 372 } |
| paddy@1 | 373 if len(data.ProfileID) > 0 { |
| paddy@1 | 374 q.Set("profile", data.ProfileID.String()) |
| paddy@1 | 375 } |
| paddy@1 | 376 if fragment { |
| paddy@1 | 377 u.RawQuery = "" |
| paddy@1 | 378 u.Fragment = q.Encode() |
| paddy@1 | 379 } else { |
| paddy@1 | 380 u.RawQuery = q.Encode() |
| paddy@1 | 381 } |
| paddy@1 | 382 |
| paddy@1 | 383 return u.String(), nil |
| paddy@1 | 384 } |
| paddy@0 | 385 |
| paddy@0 | 386 // getClient looks up and authenticates the basic auth using the given |
| paddy@0 | 387 // storage. Sets an error on the response if auth fails or a server error occurs. |
| paddy@0 | 388 func getClient(auth BasicAuth, ctx Context) (Client, error) { |
| paddy@0 | 389 id, err := uuid.Parse(auth.Username) |
| paddy@0 | 390 if err != nil { |
| paddy@0 | 391 return Client{}, err |
| paddy@0 | 392 } |
| paddy@1 | 393 client, err := ctx.Clients.GetClient(id) |
| paddy@0 | 394 if err != nil { |
| paddy@5 | 395 if err == ClientNotFoundError { |
| paddy@5 | 396 return Client{}, err |
| paddy@5 | 397 } |
| paddy@10 | 398 if ctx.Log != nil { |
| paddy@10 | 399 ctx.Log.Printf("Error retrieving client %s: %s", id, err) |
| paddy@10 | 400 } |
| paddy@5 | 401 return Client{}, InternalServerError |
| paddy@0 | 402 } |
| paddy@0 | 403 if client.Secret != auth.Password { |
| paddy@5 | 404 return Client{}, InvalidClientError |
| paddy@0 | 405 } |
| paddy@0 | 406 if client.RedirectURI == "" { |
| paddy@5 | 407 return Client{}, InvalidClientError |
| paddy@0 | 408 } |
| paddy@0 | 409 return client, nil |
| paddy@0 | 410 } |