func (s *storage) convertFromAuthorizeToken(authorize *api.OAuthAuthorizeToken) (*osin.AuthorizeData, error) { user, err := s.user.ConvertFromAuthorizeToken(authorize) if err != nil { return nil, err } client, err := s.client.GetClient(kapi.NewContext(), authorize.ClientName) if err != nil { return nil, err } if err := scopeauthorizer.ValidateScopeRestrictions(client, authorize.Scopes...); err != nil { return nil, err } return &osin.AuthorizeData{ Code: authorize.Name, CodeChallenge: authorize.CodeChallenge, CodeChallengeMethod: authorize.CodeChallengeMethod, Client: &clientWrapper{authorize.ClientName, client}, ExpiresIn: int32(authorize.ExpiresIn), Scope: scope.Join(authorize.Scopes), RedirectUri: authorize.RedirectURI, State: authorize.State, CreatedAt: authorize.CreationTimestamp.Time, UserData: user, }, nil }
func (s *storage) convertFromAccessToken(access *api.OAuthAccessToken) (*osin.AccessData, error) { user, err := s.user.ConvertFromAccessToken(access) if err != nil { return nil, err } client, err := s.client.GetClient(kapi.NewContext(), access.ClientName) if err != nil { return nil, err } return &osin.AccessData{ AccessToken: access.Name, RefreshToken: access.RefreshToken, Client: &clientWrapper{access.ClientName, client}, ExpiresIn: int32(access.ExpiresIn), Scope: scope.Join(access.Scopes), RedirectUri: access.RedirectURI, CreatedAt: access.CreationTimestamp.Time, UserData: user, }, nil }
// HandleAuthorize implements osinserver.AuthorizeHandler to ensure the requested scopes have been authorized. // The AuthorizeRequest.Authorized field must already be set to true for the grant check to occur. // If the requested scopes are authorized, the AuthorizeRequest is unchanged. // If the requested scopes are not authorized, or an error occurs, AuthorizeRequest.Authorized is set to false. // If the response is written, true is returned. // If the response is not written, false is returned. func (h *GrantCheck) HandleAuthorize(ar *osin.AuthorizeRequest, resp *osin.Response, w http.ResponseWriter) (bool, error) { // Requests must already be authorized before we will check grants if !ar.Authorized { return false, nil } // Reset request to unauthorized until we verify the grant ar.Authorized = false user, ok := ar.UserData.(user.Info) if !ok || user == nil { utilruntime.HandleError(fmt.Errorf("the provided user data is not a user.Info object: %#v", user)) resp.SetError("server_error", "") return false, nil } client, ok := ar.Client.GetUserData().(*oauthapi.OAuthClient) if !ok || client == nil { utilruntime.HandleError(fmt.Errorf("the provided client is not an *api.OAuthClient object: %#v", client)) resp.SetError("server_error", "") return false, nil } // Normalize the scope request, and ensure all tokens contain a scope scopes := scope.Split(ar.Scope) if len(scopes) == 0 { scopes = append(scopes, scopeauthorizer.UserFull) } ar.Scope = scope.Join(scopes) // Validate the requested scopes if scopeErrors := validation.ValidateScopes(scopes, nil); len(scopeErrors) > 0 { resp.SetError("invalid_scope", scopeErrors.ToAggregate().Error()) return false, nil } invalidScopes := sets.NewString() for _, scope := range scopes { if err := scopeauthorizer.ValidateScopeRestrictions(client, scope); err != nil { invalidScopes.Insert(scope) } } if len(invalidScopes) > 0 { resp.SetError("access_denied", fmt.Sprintf("scope denied: %s", strings.Join(invalidScopes.List(), " "))) return false, nil } grant := &api.Grant{ Client: ar.Client, Scope: ar.Scope, Expiration: int64(ar.Expiration), RedirectURI: ar.RedirectUri, } // Check if the user has already authorized this grant authorized, err := h.check.HasAuthorizedClient(user, grant) if err != nil { utilruntime.HandleError(err) resp.SetError("server_error", "") return false, nil } if authorized { ar.Authorized = true return false, nil } // React to an unauthorized grant authorized, handled, err := h.handler.GrantNeeded(user, grant, w, ar.HttpRequest) if authorized { ar.Authorized = true } return handled, err }
func TestSAAsOAuthClient(t *testing.T) { testutil.RequireEtcd(t) _, clusterAdminKubeConfig, err := testserver.StartTestMaster() if err != nil { t.Fatalf("unexpected error: %v", err) } authorizationCodes := make(chan string, 1) authorizationErrors := make(chan string, 1) oauthServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { t.Logf("fake pod server got %v", req.URL) if code := req.URL.Query().Get("code"); len(code) > 0 { authorizationCodes <- code } if err := req.URL.Query().Get("error"); len(err) > 0 { authorizationErrors <- err } })) defer oauthServer.Close() clusterAdminClient, err := testutil.GetClusterAdminClient(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } clusterAdminKubeClient, err := testutil.GetClusterAdminKubeClient(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } clusterAdminClientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } projectName := "hammer-project" if _, err := testserver.CreateNewProject(clusterAdminClient, *clusterAdminClientConfig, projectName, "harold"); err != nil { t.Fatalf("unexpected error: %v", err) } if err := testserver.WaitForServiceAccounts(clusterAdminKubeClient, projectName, []string{"default"}); err != nil { t.Fatalf("unexpected error: %v", err) } // get the SA ready with redirect URIs and secret annotations var defaultSA *kapi.ServiceAccount // retry this a couple times. We seem to be flaking on update conflicts and missing secrets all together err = kclient.RetryOnConflict(kclient.DefaultRetry, func() error { defaultSA, err = clusterAdminKubeClient.ServiceAccounts(projectName).Get("default") if err != nil { return err } if defaultSA.Annotations == nil { defaultSA.Annotations = map[string]string{} } defaultSA.Annotations[saoauth.OAuthRedirectURISecretAnnotationPrefix+"one"] = oauthServer.URL defaultSA.Annotations[saoauth.OAuthWantChallengesAnnotationPrefix] = "true" defaultSA, err = clusterAdminKubeClient.ServiceAccounts(projectName).Update(defaultSA) return err }) if err != nil { t.Fatalf("unexpected error: %v", err) } var oauthSecret *kapi.Secret // retry this a couple times. We seem to be flaking on update conflicts and missing secrets all together err = wait.PollImmediate(30*time.Millisecond, 10*time.Second, func() (done bool, err error) { allSecrets, err := clusterAdminKubeClient.Secrets(projectName).List(kapi.ListOptions{}) if err != nil { return false, err } for i := range allSecrets.Items { secret := allSecrets.Items[i] if serviceaccount.IsServiceAccountToken(&secret, defaultSA) { oauthSecret = &secret return true, nil } } return false, nil }) if err != nil { t.Fatalf("unexpected error: %v", err) } oauthClientConfig := &osincli.ClientConfig{ ClientId: serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name), ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]), AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: oauthServer.URL, Scope: scope.Join([]string{"user:info", "role:edit:" + projectName}), SendClientSecretInParams: true, } runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, true) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) oauthClientConfig = &osincli.ClientConfig{ ClientId: serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name), ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]), AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: oauthServer.URL, Scope: scope.Join([]string{"user:info", "role:edit:other-ns"}), SendClientSecretInParams: true, } runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, false, false) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) oauthClientConfig = &osincli.ClientConfig{ ClientId: serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name), ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]), AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: oauthServer.URL, Scope: scope.Join([]string{"user:info"}), SendClientSecretInParams: true, } runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, authorizationCodes, authorizationErrors, true, false) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) }
func TestSAAsOAuthClient(t *testing.T) { testutil.RequireEtcd(t) defer testutil.DumpEtcdOnFailure(t) _, clusterAdminKubeConfig, err := testserver.StartTestMaster() if err != nil { t.Fatalf("unexpected error: %v", err) } authorizationCodes := make(chan string, 1) authorizationErrors := make(chan string, 1) oauthServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { t.Logf("fake pod server got %v", req.URL) if code := req.URL.Query().Get("code"); len(code) > 0 { authorizationCodes <- code } if err := req.URL.Query().Get("error"); len(err) > 0 { authorizationErrors <- err } })) defer oauthServer.Close() redirectURL := oauthServer.URL + "/oauthcallback" clusterAdminClient, err := testutil.GetClusterAdminClient(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } clusterAdminKubeClient, err := testutil.GetClusterAdminKubeClient(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } clusterAdminClientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } projectName := "hammer-project" if _, err := testserver.CreateNewProject(clusterAdminClient, *clusterAdminClientConfig, projectName, "harold"); err != nil { t.Fatalf("unexpected error: %v", err) } if err := testserver.WaitForServiceAccounts(clusterAdminKubeClient, projectName, []string{"default"}); err != nil { t.Fatalf("unexpected error: %v", err) } promptingClient, err := clusterAdminClient.OAuthClients().Create(&oauthapi.OAuthClient{ ObjectMeta: kapi.ObjectMeta{Name: "prompting-client"}, Secret: "prompting-client-secret", RedirectURIs: []string{redirectURL}, GrantMethod: oauthapi.GrantHandlerPrompt, RespondWithChallenges: true, }) if err != nil { t.Fatalf("unexpected error: %v", err) } // get the SA ready with redirect URIs and secret annotations var defaultSA *kapi.ServiceAccount // retry this a couple times. We seem to be flaking on update conflicts and missing secrets all together err = kclient.RetryOnConflict(kclient.DefaultRetry, func() error { defaultSA, err = clusterAdminKubeClient.ServiceAccounts(projectName).Get("default") if err != nil { return err } if defaultSA.Annotations == nil { defaultSA.Annotations = map[string]string{} } defaultSA.Annotations[saoauth.OAuthRedirectURISecretAnnotationPrefix+"one"] = redirectURL defaultSA.Annotations[saoauth.OAuthWantChallengesAnnotationPrefix] = "true" defaultSA, err = clusterAdminKubeClient.ServiceAccounts(projectName).Update(defaultSA) return err }) if err != nil { t.Fatalf("unexpected error: %v", err) } var oauthSecret *kapi.Secret // retry this a couple times. We seem to be flaking on update conflicts and missing secrets all together err = wait.PollImmediate(30*time.Millisecond, 10*time.Second, func() (done bool, err error) { allSecrets, err := clusterAdminKubeClient.Secrets(projectName).List(kapi.ListOptions{}) if err != nil { return false, err } for i := range allSecrets.Items { secret := allSecrets.Items[i] if serviceaccount.IsServiceAccountToken(&secret, defaultSA) { oauthSecret = &secret return true, nil } } return false, nil }) if err != nil { t.Fatalf("unexpected error: %v", err) } // Test with a normal OAuth client { oauthClientConfig := &osincli.ClientConfig{ ClientId: promptingClient.Name, ClientSecret: promptingClient.Secret, AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: redirectURL, SendClientSecretInParams: true, } t.Log("Testing unrestricted scope") oauthClientConfig.Scope = "" // approval steps are needed for unscoped access runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauth/approve", "form", "POST /oauth/approve", "redirect to /oauth/authorize", "redirect to /oauthcallback", "code", "scope:user:full", }) // verify the persisted client authorization looks like we expect if clientAuth, err := clusterAdminClient.OAuthClientAuthorizations().Get("harold:" + oauthClientConfig.ClientId); err != nil { t.Fatalf("Unexpected error: %v", err) } else if !reflect.DeepEqual(clientAuth.Scopes, []string{"user:full"}) { t.Fatalf("Unexpected scopes: %v", clientAuth.Scopes) } else { // update the authorization to not contain any approved scopes clientAuth.Scopes = nil if _, err := clusterAdminClient.OAuthClientAuthorizations().Update(clientAuth); err != nil { t.Fatalf("Unexpected error: %v", err) } } // approval steps are needed again for unscoped access runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauth/approve", "form", "POST /oauth/approve", "redirect to /oauth/authorize", "redirect to /oauthcallback", "code", "scope:user:full", }) // with the authorization stored, approval steps are skipped runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauthcallback", "code", "scope:user:full", }) // Approval step is needed again t.Log("Testing restricted scope") oauthClientConfig.Scope = "user:info user:check-access" // filter to disapprove of granting the user:check-access scope deniedScope := false inputFilter := func(inputType, name, value string) bool { if inputType == "checkbox" && name == "scope" && value == "user:check-access" { deniedScope = true return false } return true } // our token only gets the approved one runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, inputFilter, authorizationCodes, authorizationErrors, true, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauth/approve", "form", "POST /oauth/approve", "redirect to /oauth/authorize", "redirect to /oauthcallback", "code", "scope:user:info", }) if !deniedScope { t.Errorf("Expected form filter to deny user:info scope") } // second time, we approve all, and our token gets all requested scopes runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauth/approve", "form", "POST /oauth/approve", "redirect to /oauth/authorize", "redirect to /oauthcallback", "code", "scope:" + oauthClientConfig.Scope, }) // third time, the approval steps is not needed, and the token gets all requested scopes runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauthcallback", "code", "scope:" + oauthClientConfig.Scope, }) // Now request an unscoped token again, and no approval should be needed t.Log("Testing unrestricted scope") oauthClientConfig.Scope = "" runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauthcallback", "code", "scope:user:full", }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } { oauthClientConfig := &osincli.ClientConfig{ ClientId: serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name), ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]), AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: redirectURL, Scope: scope.Join([]string{"user:info", "role:edit:" + projectName}), SendClientSecretInParams: true, } t.Log("Testing allowed scopes") // First time, the approval steps are needed // Second time, the approval steps are skipped runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauth/approve", "form", "POST /oauth/approve", "redirect to /oauth/authorize", "redirect to /oauthcallback", "code", "scope:" + oauthClientConfig.Scope, }) runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, true, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauthcallback", "code", "scope:" + oauthClientConfig.Scope, }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } { oauthClientConfig := &osincli.ClientConfig{ ClientId: serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name), ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]), AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: redirectURL, Scope: scope.Join([]string{"user:info", "role:edit:other-ns"}), SendClientSecretInParams: true, } t.Log("Testing disallowed scopes") runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, false, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauthcallback", "error:access_denied", }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } { t.Log("Testing invalid scopes") oauthClientConfig := &osincli.ClientConfig{ ClientId: serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name), ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]), AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: redirectURL, Scope: scope.Join([]string{"unknown-scope"}), SendClientSecretInParams: true, } runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, false, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauthcallback", "error:invalid_scope", }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } { t.Log("Testing allowed scopes with failed API call") oauthClientConfig := &osincli.ClientConfig{ ClientId: serviceaccount.MakeUsername(defaultSA.Namespace, defaultSA.Name), ClientSecret: string(oauthSecret.Data[kapi.ServiceAccountTokenKey]), AuthorizeUrl: clusterAdminClientConfig.Host + "/oauth/authorize", TokenUrl: clusterAdminClientConfig.Host + "/oauth/token", RedirectUrl: redirectURL, Scope: scope.Join([]string{"user:info"}), SendClientSecretInParams: true, } // First time, the approval is needed // Second time, the approval is skipped runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauth/approve", "form", "POST /oauth/approve", "redirect to /oauth/authorize", "redirect to /oauthcallback", "code", "scope:" + oauthClientConfig.Scope, }) runOAuthFlow(t, clusterAdminClientConfig, projectName, oauthClientConfig, nil, authorizationCodes, authorizationErrors, true, false, []string{ "GET /oauth/authorize", "received challenge", "GET /oauth/authorize", "redirect to /oauthcallback", "code", "scope:" + oauthClientConfig.Scope, }) clusterAdminClient.OAuthClientAuthorizations().Delete("harold:" + oauthClientConfig.ClientId) } }
func (l *Grant) handleGrant(user user.Info, w http.ResponseWriter, req *http.Request) { if ok, err := l.csrf.Check(req, req.FormValue(csrfParam)); !ok || err != nil { glog.Errorf("Unable to check CSRF token: %v", err) l.failed("Invalid CSRF token", w, req) return } req.ParseForm() then := req.FormValue(thenParam) scopes := scope.Join(req.Form[scopeParam]) username := req.FormValue(userNameParam) if username != user.GetName() { glog.Errorf("User (%v) did not match authenticated user (%v)", username, user.GetName()) l.failed("User did not match", w, req) return } if len(req.FormValue(approveParam)) == 0 || len(scopes) == 0 { // Redirect with an error param url, err := url.Parse(then) if len(then) == 0 || err != nil { l.failed("Access denied, but no redirect URL was specified", w, req) return } q := url.Query() q.Set("error", "access_denied") url.RawQuery = q.Encode() http.Redirect(w, req, url.String(), http.StatusFound) return } clientID := req.FormValue(clientIDParam) client, err := l.clientregistry.GetClient(kapi.NewContext(), clientID) if err != nil || client == nil { l.failed("Could not find client for client_id", w, req) return } if err := scopeauthorizer.ValidateScopeRestrictions(client, scope.Split(scopes)...); err != nil { failure := fmt.Sprintf("%v requested illegal scopes (%v): %v", client.Name, scopes, err) l.failed(failure, w, req) return } clientAuthID := l.authregistry.ClientAuthorizationName(user.GetName(), client.Name) ctx := kapi.NewContext() clientAuth, err := l.authregistry.GetClientAuthorization(ctx, clientAuthID) if err == nil && clientAuth != nil { // Add new scopes and update clientAuth.Scopes = scope.Add(clientAuth.Scopes, scope.Split(scopes)) if _, err = l.authregistry.UpdateClientAuthorization(ctx, clientAuth); err != nil { glog.Errorf("Unable to update authorization: %v", err) l.failed("Could not update client authorization", w, req) return } } else { // Make sure client name, user name, grant scope, expiration, and redirect uri match clientAuth = &oapi.OAuthClientAuthorization{ UserName: user.GetName(), UserUID: user.GetUID(), ClientName: client.Name, Scopes: scope.Split(scopes), } clientAuth.Name = clientAuthID if _, err = l.authregistry.CreateClientAuthorization(ctx, clientAuth); err != nil { glog.Errorf("Unable to create authorization: %v", err) l.failed("Could not create client authorization", w, req) return } } // Redirect, overriding the scope param on the redirect with the scopes that were actually granted url, err := url.Parse(then) if len(then) == 0 || err != nil { l.failed("Access granted, but no redirect URL was specified", w, req) return } q := url.Query() q.Set("scope", scopes) url.RawQuery = q.Encode() http.Redirect(w, req, url.String(), http.StatusFound) }