initial commit
This commit is contained in:
commit
54576d9d33
61 changed files with 5569 additions and 0 deletions
139
client/client.go
Normal file
139
client/client.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Package client provides fail-closed API-key authorization middleware for Go services.
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultHeader = "X-API-Key"
|
||||
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
Tenant string
|
||||
Application string
|
||||
Header string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
tenant string
|
||||
application string
|
||||
header string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type Principal struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
ApplicationID string `json:"application_id"`
|
||||
ApplicationSlug string `json:"application_slug"`
|
||||
APIKeyID string `json:"api_key_id"`
|
||||
APIKeyName string `json:"api_key_name"`
|
||||
}
|
||||
|
||||
type authorizationResponse struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
Reason string `json:"reason"`
|
||||
Principal
|
||||
}
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
func New(cfg Config) (*Client, error) {
|
||||
if strings.TrimSpace(cfg.BaseURL) == "" || strings.TrimSpace(cfg.Application) == "" {
|
||||
return nil, errors.New("go-loose client requires BaseURL and Application")
|
||||
}
|
||||
if cfg.Header == "" {
|
||||
cfg.Header = DefaultHeader
|
||||
}
|
||||
if cfg.HTTPClient == nil {
|
||||
cfg.HTTPClient = &http.Client{Timeout: 3 * time.Second}
|
||||
}
|
||||
return &Client{
|
||||
baseURL: strings.TrimRight(cfg.BaseURL, "/"), tenant: cfg.Tenant,
|
||||
application: cfg.Application, header: cfg.Header, httpClient: cfg.HTTPClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Authorize(ctx context.Context, apiKey, method, requestPath, host string) (Principal, error) {
|
||||
payload, err := json.Marshal(map[string]string{
|
||||
"tenant": c.tenant, "application": c.application, "method": method,
|
||||
"path": requestPath, "host": stripPort(host),
|
||||
})
|
||||
if err != nil {
|
||||
return Principal{}, fmt.Errorf("encode authorization request: %w", err)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/authorize", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return Principal{}, fmt.Errorf("create authorization request: %w", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set(c.header, apiKey)
|
||||
response, err := c.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return Principal{}, fmt.Errorf("authorize with Go Loose: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
||||
if err != nil {
|
||||
return Principal{}, fmt.Errorf("read authorization response: %w", err)
|
||||
}
|
||||
var result authorizationResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return Principal{}, fmt.Errorf("decode authorization response (HTTP %d): %w", response.StatusCode, err)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || !result.Allowed {
|
||||
if result.Reason == "" {
|
||||
result.Reason = http.StatusText(response.StatusCode)
|
||||
}
|
||||
return Principal{}, fmt.Errorf("request denied by Go Loose: %s", result.Reason)
|
||||
}
|
||||
return result.Principal, nil
|
||||
}
|
||||
|
||||
// Middleware rejects missing, invalid, or unverifiable keys and places the
|
||||
// authorized Principal in the request context.
|
||||
func (c *Client) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
apiKey := strings.TrimSpace(r.Header.Get(c.header))
|
||||
if apiKey == "" {
|
||||
writeMiddlewareError(w, http.StatusUnauthorized, "missing API key")
|
||||
return
|
||||
}
|
||||
principal, err := c.Authorize(r.Context(), apiKey, r.Method, r.URL.Path, r.Host)
|
||||
if err != nil {
|
||||
writeMiddlewareError(w, http.StatusForbidden, "API key not authorized")
|
||||
return
|
||||
}
|
||||
r.Header.Del(c.header)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKey{}, principal)))
|
||||
})
|
||||
}
|
||||
|
||||
func PrincipalFromContext(ctx context.Context) (Principal, bool) {
|
||||
principal, ok := ctx.Value(contextKey{}).(Principal)
|
||||
return principal, ok
|
||||
}
|
||||
|
||||
func stripPort(host string) string {
|
||||
if parsed, _, err := net.SplitHostPort(host); err == nil {
|
||||
return strings.ToLower(parsed)
|
||||
}
|
||||
return strings.ToLower(host)
|
||||
}
|
||||
|
||||
func writeMiddlewareError(w http.ResponseWriter, status int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
54
client/client_test.go
Normal file
54
client/client_test.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMiddlewareAuthorizesAndAddsPrincipal(t *testing.T) {
|
||||
authorizationServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(DefaultHeader) != "gl_test_key_that_is_long_enough_for_client" {
|
||||
t.Errorf("unexpected API key")
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"allowed":true,"tenant_id":"tenant-1","tenant_slug":"nmbs","application_id":"app-1","application_slug":"guess","api_key_id":"key-1","api_key_name":"test"}`))
|
||||
}))
|
||||
defer authorizationServer.Close()
|
||||
|
||||
client, err := New(Config{BaseURL: authorizationServer.URL, Tenant: "nmbs", Application: "guess"})
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
handler := client.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := PrincipalFromContext(r.Context())
|
||||
if !ok || principal.TenantSlug != "nmbs" {
|
||||
t.Fatalf("missing principal: %#v", principal)
|
||||
}
|
||||
if r.Header.Get(DefaultHeader) != "" {
|
||||
t.Fatal("middleware must not forward the credential")
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
request := httptest.NewRequest(http.MethodGet, "http://nmbs.guess.local/guesses", nil)
|
||||
request.Header.Set(DefaultHeader, "gl_test_key_that_is_long_enough_for_client")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusNoContent {
|
||||
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareFailsClosedWithoutKey(t *testing.T) {
|
||||
client, err := New(Config{BaseURL: "http://go-loose", Application: "guess"})
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
called := false
|
||||
handler := client.Middleware(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if recorder.Code != http.StatusUnauthorized || called {
|
||||
t.Fatalf("status = %d, called = %v", recorder.Code, called)
|
||||
}
|
||||
}
|
||||
219
client/webauth.go
Normal file
219
client/webauth.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSessionCookie = "go_loose_user_session"
|
||||
stateCookie = "go_loose_login_state"
|
||||
)
|
||||
|
||||
type BrowserAuthConfig struct {
|
||||
BaseURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
RedirectURL string
|
||||
AfterLoginURL string
|
||||
AfterLogoutURL string
|
||||
LoginPath string
|
||||
CookieName string
|
||||
CookieSecure bool
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
type BrowserAuth struct {
|
||||
config BrowserAuthConfig
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"display_name"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
ApplicationID string `json:"application_id"`
|
||||
Application string `json:"application"`
|
||||
}
|
||||
|
||||
type userContextKey struct{}
|
||||
|
||||
func NewBrowserAuth(config BrowserAuthConfig) (*BrowserAuth, error) {
|
||||
if config.BaseURL == "" || config.ClientID == "" || config.ClientSecret == "" || config.RedirectURL == "" {
|
||||
return nil, errors.New("browser auth requires BaseURL, ClientID, ClientSecret, and RedirectURL")
|
||||
}
|
||||
redirect, err := url.Parse(config.RedirectURL)
|
||||
if err != nil || !redirect.IsAbs() {
|
||||
return nil, errors.New("RedirectURL must be absolute")
|
||||
}
|
||||
config.BaseURL = strings.TrimRight(config.BaseURL, "/")
|
||||
if config.AfterLoginURL == "" {
|
||||
config.AfterLoginURL = "/"
|
||||
}
|
||||
if config.AfterLogoutURL == "" {
|
||||
config.AfterLogoutURL = "/"
|
||||
}
|
||||
if config.LoginPath == "" {
|
||||
config.LoginPath = "/login"
|
||||
}
|
||||
if config.CookieName == "" {
|
||||
config.CookieName = defaultSessionCookie
|
||||
}
|
||||
if config.HTTPClient == nil {
|
||||
config.HTTPClient = &http.Client{Timeout: 5 * time.Second}
|
||||
}
|
||||
return &BrowserAuth{config: config}, nil
|
||||
}
|
||||
|
||||
func (a *BrowserAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := randomValue(24)
|
||||
if err != nil {
|
||||
http.Error(w, "could not start login", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: stateCookie, Value: state, Path: callbackPath(a.config.RedirectURL),
|
||||
HttpOnly: true, Secure: a.config.CookieSecure, SameSite: http.SameSiteLaxMode, MaxAge: 600,
|
||||
})
|
||||
query := url.Values{
|
||||
"response_type": {"code"}, "client_id": {a.config.ClientID},
|
||||
"redirect_uri": {a.config.RedirectURL}, "state": {state},
|
||||
}
|
||||
http.Redirect(w, r, a.config.BaseURL+"/connect/authorize?"+query.Encode(), http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *BrowserAuth) CallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := r.Cookie(stateCookie)
|
||||
if err != nil || state.Value == "" || subtle.ConstantTimeCompare([]byte(state.Value), []byte(r.URL.Query().Get("state"))) != 1 {
|
||||
http.Error(w, "invalid login state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
http.Error(w, "authorization code missing", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
form := url.Values{
|
||||
"grant_type": {"authorization_code"}, "code": {code},
|
||||
"redirect_uri": {a.config.RedirectURL},
|
||||
}
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodPost, a.config.BaseURL+"/connect/token", strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
http.Error(w, "could not complete login", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.SetBasicAuth(a.config.ClientID, a.config.ClientSecret)
|
||||
response, err := a.config.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
http.Error(w, "identity service unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&tokenResponse); err != nil || response.StatusCode != http.StatusOK || tokenResponse.AccessToken == "" {
|
||||
http.Error(w, "authorization code exchange failed", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: a.config.CookieName, Value: tokenResponse.AccessToken, Path: "/",
|
||||
HttpOnly: true, Secure: a.config.CookieSecure, SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: tokenResponse.ExpiresIn,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{Name: stateCookie, Value: "", Path: callbackPath(a.config.RedirectURL), MaxAge: -1, HttpOnly: true})
|
||||
http.Redirect(w, r, a.config.AfterLoginURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *BrowserAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var revokeErr error
|
||||
if cookie, err := r.Cookie(a.config.CookieName); err == nil {
|
||||
request, requestErr := http.NewRequestWithContext(r.Context(), http.MethodPost, a.config.BaseURL+"/connect/logout", nil)
|
||||
if requestErr != nil {
|
||||
revokeErr = requestErr
|
||||
} else {
|
||||
request.Header.Set("Authorization", "Bearer "+cookie.Value)
|
||||
response, responseErr := a.config.HTTPClient.Do(request)
|
||||
if responseErr != nil {
|
||||
revokeErr = responseErr
|
||||
} else {
|
||||
response.Body.Close()
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
revokeErr = fmt.Errorf("Go Loose logout returned HTTP %d", response.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: a.config.CookieName, Value: "", Path: "/", MaxAge: -1,
|
||||
HttpOnly: true, Secure: a.config.CookieSecure, SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
if revokeErr != nil {
|
||||
http.Error(w, "local session cleared, but central session revocation failed", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, a.config.AfterLogoutURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *BrowserAuth) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(a.config.CookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
http.Redirect(w, r, a.config.LoginPath, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, a.config.BaseURL+"/connect/userinfo", nil)
|
||||
if err != nil {
|
||||
http.Error(w, "could not validate session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
request.Header.Set("Authorization", "Bearer "+cookie.Value)
|
||||
response, err := a.config.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
http.Error(w, "identity service unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var user User
|
||||
if response.StatusCode != http.StatusOK || json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&user) != nil {
|
||||
http.SetCookie(w, &http.Cookie{Name: a.config.CookieName, Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
|
||||
http.Redirect(w, r, a.config.LoginPath, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userContextKey{}, user)))
|
||||
})
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) (User, bool) {
|
||||
user, ok := ctx.Value(userContextKey{}).(User)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
func randomValue(size int) (string, error) {
|
||||
value := make([]byte, size)
|
||||
if _, err := rand.Read(value); err != nil {
|
||||
return "", fmt.Errorf("generate random value: %w", err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(value), nil
|
||||
}
|
||||
|
||||
func callbackPath(rawURL string) string {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed.Path == "" {
|
||||
return "/"
|
||||
}
|
||||
return parsed.Path
|
||||
}
|
||||
101
client/webauth_test.go
Normal file
101
client/webauth_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBrowserLoginRedirect(t *testing.T) {
|
||||
auth, err := NewBrowserAuth(BrowserAuthConfig{
|
||||
BaseURL: "https://nmbs.goloose.local", ClientID: "client",
|
||||
ClientSecret: "secret", RedirectURL: "https://nmbs.guess.local/auth/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewBrowserAuth() error = %v", err)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
auth.LoginHandler(recorder, httptest.NewRequest(http.MethodGet, "/login", nil))
|
||||
if recorder.Code != http.StatusFound {
|
||||
t.Fatalf("status = %d", recorder.Code)
|
||||
}
|
||||
target, err := url.Parse(recorder.Header().Get("Location"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse location: %v", err)
|
||||
}
|
||||
if target.Path != "/connect/authorize" || target.Query().Get("client_id") != "client" || target.Query().Get("state") == "" {
|
||||
t.Fatalf("unexpected redirect: %s", target)
|
||||
}
|
||||
if len(recorder.Result().Cookies()) != 1 || !recorder.Result().Cookies()[0].HttpOnly {
|
||||
t.Fatal("expected an HTTP-only state cookie")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowserMiddlewareAddsUser(t *testing.T) {
|
||||
identity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer session-token" {
|
||||
t.Fatal("missing bearer token")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"id":"user-1","email":"user@example.com","tenant_slug":"nmbs","application":"guess"}`))
|
||||
}))
|
||||
defer identity.Close()
|
||||
auth, err := NewBrowserAuth(BrowserAuthConfig{
|
||||
BaseURL: identity.URL, ClientID: "client", ClientSecret: "secret",
|
||||
RedirectURL: "http://localhost/auth/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewBrowserAuth() error = %v", err)
|
||||
}
|
||||
handler := auth.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := UserFromContext(r.Context())
|
||||
if !ok || user.Email != "user@example.com" {
|
||||
t.Fatalf("unexpected user: %#v", user)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
request := httptest.NewRequest(http.MethodGet, "http://localhost/private", nil)
|
||||
request.AddCookie(&http.Cookie{Name: defaultSessionCookie, Value: "session-token"})
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusNoContent {
|
||||
t.Fatalf("status = %d", recorder.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowserCallbackUsesBasicClientAuthentication(t *testing.T) {
|
||||
identity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientID, secret, ok := r.BasicAuth()
|
||||
if !ok || clientID != "client-id" || secret != "client-secret" {
|
||||
t.Fatalf("unexpected client authentication: %q, %q, %v", clientID, secret, ok)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":"session-token","expires_in":3600}`))
|
||||
}))
|
||||
defer identity.Close()
|
||||
auth, err := NewBrowserAuth(BrowserAuthConfig{
|
||||
BaseURL: identity.URL, ClientID: "client-id", ClientSecret: "client-secret",
|
||||
RedirectURL: "http://localhost/auth/callback",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewBrowserAuth() error = %v", err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodGet, "/auth/callback?code=one-time-code&state=login-state", nil)
|
||||
request.AddCookie(&http.Cookie{Name: stateCookie, Value: "login-state"})
|
||||
recorder := httptest.NewRecorder()
|
||||
auth.CallbackHandler(recorder, request)
|
||||
if recorder.Code != http.StatusSeeOther {
|
||||
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var session *http.Cookie
|
||||
for _, cookie := range recorder.Result().Cookies() {
|
||||
if cookie.Name == defaultSessionCookie {
|
||||
session = cookie
|
||||
}
|
||||
}
|
||||
if session == nil || session.Value != "session-token" || !session.HttpOnly {
|
||||
t.Fatalf("unexpected session cookie: %#v", session)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue