mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-25 06:57:11 +02:00
frontend: react+mui (#3004)
* mui v5 wip * wip * wip * wip * use compressor for all controlplane endpoints * wip * wip * add deps * fix authenticate URL * fix test * fix test * fix build * maybe fix build * fix integration test * remove image asset test * add yarn.lock
This commit is contained in:
parent
64d8748251
commit
2824faecbf
84 changed files with 13373 additions and 1455 deletions
|
@ -6,11 +6,8 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
|
||||
"github.com/pomerium/pomerium/config"
|
||||
"github.com/pomerium/pomerium/internal/frontend"
|
||||
"github.com/pomerium/pomerium/internal/identity"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
|
@ -48,8 +45,6 @@ func ValidateOptions(o *config.Options) error {
|
|||
|
||||
// Authenticate contains data required to run the authenticate service.
|
||||
type Authenticate struct {
|
||||
templates *template.Template
|
||||
|
||||
options *config.AtomicOptions
|
||||
provider *identity.AtomicAuthenticator
|
||||
state *atomicAuthenticateState
|
||||
|
@ -58,10 +53,9 @@ type Authenticate struct {
|
|||
// New validates and creates a new authenticate service from a set of Options.
|
||||
func New(cfg *config.Config) (*Authenticate, error) {
|
||||
a := &Authenticate{
|
||||
templates: template.Must(frontend.NewTemplates()),
|
||||
options: config.NewAtomicOptions(),
|
||||
provider: identity.NewAtomicAuthenticator(),
|
||||
state: newAtomicAuthenticateState(newAuthenticateState()),
|
||||
options: config.NewAtomicOptions(),
|
||||
provider: identity.NewAtomicAuthenticator(),
|
||||
state: newAtomicAuthenticateState(newAuthenticateState()),
|
||||
}
|
||||
|
||||
state, err := newAuthenticateStateFromConfig(cfg)
|
||||
|
@ -123,17 +117,3 @@ func (a *Authenticate) updateProvider(cfg *config.Config) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildURLValues creates a new url.Values map by traversing the keys in `defaults` and using the values
|
||||
// from `values` if they exist, otherwise the provided defaults
|
||||
func buildURLValues(values, defaults url.Values) url.Values {
|
||||
result := make(url.Values)
|
||||
for k, vs := range defaults {
|
||||
if values.Has(k) {
|
||||
result[k] = values[k]
|
||||
} else if vs != nil {
|
||||
result[k] = vs
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import (
|
|||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
)
|
||||
|
||||
// Handler returns the authenticate service's handler chain.
|
||||
|
@ -99,7 +100,27 @@ func (a *Authenticate) mountDashboard(r *mux.Router) {
|
|||
sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
|
||||
sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut))
|
||||
sr.Path("/webauthn").Handler(webauthn.New(a.getWebauthnState))
|
||||
sr.Path("/device-enrolled").Handler(handlers.DeviceEnrolled())
|
||||
sr.Path("/device-enrolled").Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
authenticateURL, err := a.options.Load().GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handlers.DeviceEnrolled(authenticateURL, a.state.Load().sharedKey).ServeHTTP(w, r)
|
||||
return nil
|
||||
}))
|
||||
for _, fileName := range []string{
|
||||
"apple-touch-icon.png",
|
||||
"favicon-16x16.png",
|
||||
"favicon-32x32.png",
|
||||
"favicon.ico",
|
||||
"index.css",
|
||||
"index.js",
|
||||
} {
|
||||
fileName := fileName
|
||||
sr.Path("/" + fileName).Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return ui.ServeFile(w, r, fileName)
|
||||
}))
|
||||
}
|
||||
|
||||
cr := sr.PathPrefix("/callback").Subrouter()
|
||||
cr.Use(func(h http.Handler) http.Handler {
|
||||
|
@ -463,6 +484,11 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
state := a.state.Load()
|
||||
|
||||
authenticateURL, err := a.options.Load().GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := a.getSessionFromCtx(ctx)
|
||||
if err != nil {
|
||||
s.ID = uuid.New().String()
|
||||
|
@ -500,52 +526,17 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
|||
groups = append(groups, pbDirectoryGroup)
|
||||
}
|
||||
|
||||
signoutURL, err := a.getSignOutURL(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid signout url: %w", err)
|
||||
}
|
||||
|
||||
webAuthnURL, err := a.getWebAuthnURL(r.URL.Query())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid webauthn url: %w", err)
|
||||
}
|
||||
|
||||
type DeviceCredentialInfo struct {
|
||||
ID string
|
||||
}
|
||||
var currentDeviceCredentials, otherDeviceCredentials []DeviceCredentialInfo
|
||||
for _, id := range pbUser.GetDeviceCredentialIds() {
|
||||
selected := false
|
||||
for _, c := range pbSession.GetDeviceCredentials() {
|
||||
if c.GetId() == id {
|
||||
selected = true
|
||||
}
|
||||
}
|
||||
if selected {
|
||||
currentDeviceCredentials = append(currentDeviceCredentials, DeviceCredentialInfo{
|
||||
ID: id,
|
||||
})
|
||||
} else {
|
||||
otherDeviceCredentials = append(otherDeviceCredentials, DeviceCredentialInfo{
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
input := map[string]interface{}{
|
||||
"IsImpersonated": isImpersonated,
|
||||
"State": s, // local session state (cookie, header, etc)
|
||||
"Session": pbSession, // current access, refresh, id token
|
||||
"User": pbUser, // user details inferred from oidc id_token
|
||||
"CurrentDeviceCredentials": currentDeviceCredentials,
|
||||
"OtherDeviceCredentials": otherDeviceCredentials,
|
||||
"DirectoryUser": pbDirectoryUser, // user details inferred from idp directory
|
||||
"DirectoryGroups": groups, // user's groups inferred from idp directory
|
||||
"csrfField": csrf.TemplateField(r),
|
||||
"SignOutURL": signoutURL,
|
||||
"WebAuthnURL": webAuthnURL,
|
||||
}
|
||||
return a.templates.ExecuteTemplate(w, "userInfo.html", input)
|
||||
handlers.UserInfo(handlers.UserInfoData{
|
||||
CSRFToken: csrf.Token(r),
|
||||
DirectoryGroups: groups,
|
||||
DirectoryUser: pbDirectoryUser,
|
||||
IsImpersonated: isImpersonated,
|
||||
Session: pbSession,
|
||||
SignOutURL: urlutil.SignOutURL(r, authenticateURL, state.sharedKey),
|
||||
User: pbUser,
|
||||
WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()),
|
||||
}).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Authenticate) saveSessionToDataBroker(
|
||||
|
@ -682,12 +673,18 @@ func (a *Authenticate) getWebauthnState(ctx context.Context) (*webauthn.State, e
|
|||
return nil, err
|
||||
}
|
||||
|
||||
authenticateURL, err := a.options.Load().GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pomeriumDomains, err := a.options.Load().GetAllRouteableHTTPDomains()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &webauthn.State{
|
||||
AuthenticateURL: authenticateURL,
|
||||
SharedKey: state.sharedKey,
|
||||
Client: state.dataBrokerClient,
|
||||
PomeriumDomains: pomeriumDomains,
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/frontend"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
)
|
||||
|
||||
// DeviceEnrolled displays an HTML page informing the user that they've successfully enrolled a device.
|
||||
func DeviceEnrolled() http.Handler {
|
||||
tpl := template.Must(frontend.NewTemplates())
|
||||
type TemplateData struct{}
|
||||
func DeviceEnrolled(authenticateURL *url.URL, sharedKey []byte) http.Handler {
|
||||
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return tpl.ExecuteTemplate(w, "device-enrolled.html", TemplateData{})
|
||||
return ui.ServePage(w, r, "DeviceEnrolled", map[string]interface{}{
|
||||
"signOutUrl": urlutil.SignOutURL(r, authenticateURL, sharedKey),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
59
authenticate/handlers/userinfo.go
Normal file
59
authenticate/handlers/userinfo.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/directory"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
)
|
||||
|
||||
// UserInfoData is the data for the UserInfo page.
|
||||
type UserInfoData struct {
|
||||
CSRFToken string
|
||||
DirectoryGroups []*directory.Group
|
||||
DirectoryUser *directory.User
|
||||
IsImpersonated bool
|
||||
Session *session.Session
|
||||
SignOutURL string
|
||||
User *user.User
|
||||
WebAuthnURL string
|
||||
}
|
||||
|
||||
// ToJSON converts the data into a JSON map.
|
||||
func (data UserInfoData) ToJSON() map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
m["csrfToken"] = data.CSRFToken
|
||||
var directoryGroups []json.RawMessage
|
||||
for _, directoryGroup := range data.DirectoryGroups {
|
||||
if bs, err := protojson.Marshal(directoryGroup); err == nil {
|
||||
directoryGroups = append(directoryGroups, json.RawMessage(bs))
|
||||
}
|
||||
}
|
||||
m["directoryGroups"] = directoryGroups
|
||||
if bs, err := protojson.Marshal(data.DirectoryUser); err == nil {
|
||||
m["directoryUser"] = json.RawMessage(bs)
|
||||
}
|
||||
m["isImpersonated"] = data.IsImpersonated
|
||||
if bs, err := protojson.Marshal(data.Session); err == nil {
|
||||
m["session"] = json.RawMessage(bs)
|
||||
}
|
||||
m["signOutUrl"] = data.SignOutURL
|
||||
if bs, err := protojson.Marshal(data.User); err == nil {
|
||||
m["user"] = json.RawMessage(bs)
|
||||
}
|
||||
m["webAuthnUrl"] = data.WebAuthnURL
|
||||
return m
|
||||
}
|
||||
|
||||
// UserInfo returns a handler that renders the user info page.
|
||||
func UserInfo(data UserInfoData) http.Handler {
|
||||
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return ui.ServePage(w, r, "UserInfo", data.ToJSON())
|
||||
})
|
||||
}
|
|
@ -8,21 +8,17 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pomerium/csrf"
|
||||
"github.com/pomerium/webauthn"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
||||
"github.com/pomerium/pomerium/internal/frontend"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
|
@ -33,6 +29,7 @@ import (
|
|||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||
"github.com/pomerium/pomerium/pkg/webauthnutil"
|
||||
"github.com/pomerium/pomerium/ui"
|
||||
)
|
||||
|
||||
const maxAuthenticateResponses = 5
|
||||
|
@ -50,13 +47,14 @@ var (
|
|||
|
||||
// State is the state needed by the Handler to handle requests.
|
||||
type State struct {
|
||||
SharedKey []byte
|
||||
AuthenticateURL *url.URL
|
||||
Client databroker.DataBrokerServiceClient
|
||||
PomeriumDomains []string
|
||||
RelyingParty *webauthn.RelyingParty
|
||||
Session *session.Session
|
||||
SessionState *sessions.State
|
||||
SessionStore sessions.SessionStore
|
||||
RelyingParty *webauthn.RelyingParty
|
||||
SharedKey []byte
|
||||
}
|
||||
|
||||
// A StateProvider provides state for the handler.
|
||||
|
@ -64,15 +62,13 @@ type StateProvider = func(context.Context) (*State, error)
|
|||
|
||||
// Handler is the WebAuthn device handler.
|
||||
type Handler struct {
|
||||
getState StateProvider
|
||||
templates *template.Template
|
||||
getState StateProvider
|
||||
}
|
||||
|
||||
// New creates a new Handler.
|
||||
func New(getState StateProvider) *Handler {
|
||||
return &Handler{
|
||||
getState: getState,
|
||||
templates: template.Must(frontend.NewTemplates()),
|
||||
getState: getState,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,23 +369,12 @@ func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *Stat
|
|||
creationOptions := webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u)
|
||||
requestOptions := webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = h.templates.ExecuteTemplate(&buf, "webauthn.html", map[string]interface{}{
|
||||
"csrfField": csrf.TemplateField(r),
|
||||
"Data": map[string]interface{}{
|
||||
"creationOptions": creationOptions,
|
||||
"requestOptions": requestOptions,
|
||||
},
|
||||
"SelfURL": r.URL.String(),
|
||||
return ui.ServePage(w, r, "WebAuthnRegistration", map[string]interface{}{
|
||||
"creationOptions": creationOptions,
|
||||
"requestOptions": requestOptions,
|
||||
"selfUrl": r.URL.String(),
|
||||
"signOutUrl": urlutil.SignOutURL(r, state.AuthenticateURL, state.SharedKey),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = io.Copy(w, &buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Handler) saveSessionAndRedirect(w http.ResponseWriter, r *http.Request, state *State, rawRedirectURI string) error {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -29,7 +28,6 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/encoding"
|
||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
||||
"github.com/pomerium/pomerium/internal/encoding/mock"
|
||||
"github.com/pomerium/pomerium/internal/frontend"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
|
@ -49,7 +47,6 @@ func testAuthenticate() *Authenticate {
|
|||
redirectURL: redirectURL,
|
||||
cookieSecret: cryptutil.NewKey(),
|
||||
})
|
||||
auth.templates = template.Must(frontend.NewTemplates())
|
||||
auth.options = config.NewAtomicOptions()
|
||||
auth.options.Store(&config.Options{
|
||||
SharedKey: cryptutil.NewBase64Key(),
|
||||
|
@ -268,9 +265,8 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
|||
},
|
||||
directoryClient: new(mockDirectoryServiceClient),
|
||||
}),
|
||||
templates: template.Must(frontend.NewTemplates()),
|
||||
options: config.NewAtomicOptions(),
|
||||
provider: identity.NewAtomicAuthenticator(),
|
||||
options: config.NewAtomicOptions(),
|
||||
provider: identity.NewAtomicAuthenticator(),
|
||||
}
|
||||
if tt.signoutRedirectURL != "" {
|
||||
opts := a.options.Load()
|
||||
|
@ -671,7 +667,6 @@ func TestAuthenticate_userInfo(t *testing.T) {
|
|||
},
|
||||
directoryClient: new(mockDirectoryServiceClient),
|
||||
}),
|
||||
templates: template.Must(frontend.NewTemplates()),
|
||||
}
|
||||
r := httptest.NewRequest(tt.method, tt.url.String(), nil)
|
||||
state, err := tt.sessionStore.LoadSession(r)
|
||||
|
@ -761,7 +756,6 @@ func TestAuthenticate_SignOut_CSRF(t *testing.T) {
|
|||
},
|
||||
directoryClient: new(mockDirectoryServiceClient),
|
||||
}),
|
||||
templates: template.Must(frontend.NewTemplates()),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
package authenticate
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/pkg/webauthnutil"
|
||||
)
|
||||
|
||||
func (a *Authenticate) getRedirectURI(r *http.Request) (string, bool) {
|
||||
if v := r.FormValue(urlutil.QueryRedirectURI); v != "" {
|
||||
return v, true
|
||||
}
|
||||
|
||||
if c, err := r.Cookie(urlutil.QueryRedirectURI); err == nil {
|
||||
return c.Value, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (a *Authenticate) getSignOutURL(r *http.Request) (*url.URL, error) {
|
||||
uri, err := a.options.Load().GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri = uri.ResolveReference(&url.URL{
|
||||
Path: "/.pomerium/sign_out",
|
||||
})
|
||||
if redirectURI, ok := a.getRedirectURI(r); ok {
|
||||
uri.RawQuery = (&url.Values{
|
||||
urlutil.QueryRedirectURI: {redirectURI},
|
||||
}).Encode()
|
||||
}
|
||||
return urlutil.NewSignedURL(a.state.Load().sharedKey, uri).Sign(), nil
|
||||
}
|
||||
|
||||
func (a *Authenticate) getWebAuthnURL(values url.Values) (*url.URL, error) {
|
||||
uri, err := a.options.Load().GetAuthenticateURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri = uri.ResolveReference(&url.URL{
|
||||
Path: "/.pomerium/webauthn",
|
||||
RawQuery: buildURLValues(values, url.Values{
|
||||
urlutil.QueryDeviceType: {webauthnutil.DefaultDeviceType},
|
||||
urlutil.QueryEnrollmentToken: nil,
|
||||
urlutil.QueryRedirectURI: {uri.ResolveReference(&url.URL{
|
||||
Path: "/.pomerium/device-enrolled",
|
||||
}).String()},
|
||||
}).Encode(),
|
||||
})
|
||||
return urlutil.NewSignedURL(a.state.Load().sharedKey, uri).Sign(), nil
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package authenticate
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
)
|
||||
|
||||
func TestAuthenticate_getRedirectURI(t *testing.T) {
|
||||
t.Run("query", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "https://www.example.com?"+(url.Values{
|
||||
urlutil.QueryRedirectURI: {"https://www.example.com/redirect"},
|
||||
}).Encode(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
a := new(Authenticate)
|
||||
redirectURI, ok := a.getRedirectURI(r)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "https://www.example.com/redirect", redirectURI)
|
||||
})
|
||||
t.Run("form", func(t *testing.T) {
|
||||
r, err := http.NewRequest("POST", "https://www.example.com", strings.NewReader((url.Values{
|
||||
urlutil.QueryRedirectURI: {"https://www.example.com/redirect"},
|
||||
}).Encode()))
|
||||
require.NoError(t, err)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
a := new(Authenticate)
|
||||
redirectURI, ok := a.getRedirectURI(r)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "https://www.example.com/redirect", redirectURI)
|
||||
})
|
||||
t.Run("cookie", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "https://www.example.com", nil)
|
||||
require.NoError(t, err)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: urlutil.QueryRedirectURI,
|
||||
Value: "https://www.example.com/redirect",
|
||||
})
|
||||
|
||||
a := new(Authenticate)
|
||||
redirectURI, ok := a.getRedirectURI(r)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "https://www.example.com/redirect", redirectURI)
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue