mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-04 09:19:39 +02:00
authenticate/proxy: add user impersonation, refresh, dashboard (#123)
proxy: Add user dashboard. [GH-123] proxy/authenticate: Add manual refresh of their session. [GH-73] authorize: Add administrator (super user) account support. [GH-110] internal/policy: Allow administrators to impersonate other users. [GH-110]
This commit is contained in:
parent
dc2eb9668c
commit
66b4c2d3cd
42 changed files with 1644 additions and 1006 deletions
|
@ -94,6 +94,10 @@ type Options struct {
|
|||
|
||||
Policies []policy.Policy
|
||||
|
||||
// Administrators contains a set of emails with users who have super user
|
||||
// (sudo) access including the ability to impersonate other users' access
|
||||
Administrators []string `mapstructure:"administrators"`
|
||||
|
||||
// AuthenticateInternalAddr is used as an override when using a load balancer
|
||||
// or ingress that does not natively support routing gRPC.
|
||||
AuthenticateInternalAddr string `mapstructure:"authenticate_internal_url"`
|
||||
|
@ -116,12 +120,15 @@ type Options struct {
|
|||
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
|
||||
Headers map[string]string `mapstructure:"headers"`
|
||||
|
||||
// RefreshCooldown limits the rate a user can refresh her session
|
||||
RefreshCooldown time.Duration `mapstructure:"refresh_cooldown"`
|
||||
|
||||
// Sub-routes
|
||||
Routes map[string]string `mapstructure:"routes"`
|
||||
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"`
|
||||
}
|
||||
|
||||
// NewOptions returns a new options struct with default vaules
|
||||
// NewOptions returns a new options struct with default values
|
||||
func NewOptions() *Options {
|
||||
o := &Options{
|
||||
Debug: false,
|
||||
|
@ -148,6 +155,7 @@ func NewOptions() *Options {
|
|||
IdleTimeout: 5 * time.Minute,
|
||||
AuthenticateURL: new(url.URL),
|
||||
AuthorizeURL: new(url.URL),
|
||||
RefreshCooldown: time.Duration(5 * time.Minute),
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
|
28
internal/cryptutil/mock_cipher.go
Normal file
28
internal/cryptutil/mock_cipher.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package cryptutil // import "github.com/pomerium/pomerium/internal/cryptutil"
|
||||
|
||||
// MockCipher MockCSRFStore is a mock implementation of Cipher.
|
||||
type MockCipher struct {
|
||||
EncryptResponse []byte
|
||||
EncryptError error
|
||||
DecryptResponse []byte
|
||||
DecryptError error
|
||||
MarshalResponse string
|
||||
MarshalError error
|
||||
UnmarshalError error
|
||||
}
|
||||
|
||||
// Encrypt is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Encrypt(b []byte) ([]byte, error) { return mc.EncryptResponse, mc.EncryptError }
|
||||
|
||||
// Decrypt is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Decrypt(b []byte) ([]byte, error) { return mc.DecryptResponse, mc.DecryptError }
|
||||
|
||||
// Marshal is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Marshal(i interface{}) (string, error) {
|
||||
return mc.MarshalResponse, mc.MarshalError
|
||||
}
|
||||
|
||||
// Unmarshal is a mock implementation of MockCipher.
|
||||
func (mc MockCipher) Unmarshal(s string, i interface{}) error {
|
||||
return mc.UnmarshalError
|
||||
}
|
|
@ -30,7 +30,11 @@ func CodeForError(err error) int {
|
|||
}
|
||||
|
||||
// ErrorResponse renders an error page for errors given a message and a status code.
|
||||
// If no message is passed, defaults to the text of the status code.
|
||||
func ErrorResponse(rw http.ResponseWriter, req *http.Request, message string, code int) {
|
||||
if message == "" {
|
||||
message = http.StatusText(code)
|
||||
}
|
||||
if req.Header.Get("Accept") == "application/json" {
|
||||
var response struct {
|
||||
Error string `json:"error"`
|
||||
|
|
|
@ -166,14 +166,13 @@ func (p *GoogleProvider) Authenticate(code string) (*sessions.SessionState, erro
|
|||
}
|
||||
|
||||
return &sessions.SessionState{
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -109,14 +109,13 @@ func (p *AzureProvider) Authenticate(code string) (*sessions.SessionState, error
|
|||
}
|
||||
|
||||
return &sessions.SessionState{
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -82,12 +82,11 @@ func New(providerName string, p *Provider) (a Authenticator, err error) {
|
|||
type Provider struct {
|
||||
ProviderName string
|
||||
|
||||
RedirectURL *url.URL
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL string
|
||||
Scopes []string
|
||||
SessionLifetimeTTL time.Duration
|
||||
RedirectURL *url.URL
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL string
|
||||
Scopes []string
|
||||
|
||||
// Some providers, such as google, require additional remote api calls to retrieve
|
||||
// user details like groups. Provider is responsible for parsing.
|
||||
|
@ -156,14 +155,13 @@ func (p *Provider) Authenticate(code string) (*sessions.SessionState, error) {
|
|||
}
|
||||
|
||||
return &sessions.SessionState{
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
LifetimeDeadline: sessions.ExtendDeadline(p.SessionLifetimeTTL),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: claims.Groups,
|
||||
IDToken: rawIDToken,
|
||||
AccessToken: oauth2Token.AccessToken,
|
||||
RefreshToken: oauth2Token.RefreshToken,
|
||||
RefreshDeadline: oauth2Token.Expiry.Truncate(time.Second),
|
||||
Email: claims.Email,
|
||||
User: idToken.Subject,
|
||||
Groups: claims.Groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -208,21 +208,19 @@ func TestCookieStore_SaveSession(t *testing.T) {
|
|||
}{
|
||||
{"good",
|
||||
&SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}, cipher, false, false},
|
||||
{"bad cipher",
|
||||
&SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}, mockCipher{}, true, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package sessions // import "github.com/pomerium/pomerium/internal/sessions"
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
|
@ -14,21 +18,17 @@ var (
|
|||
|
||||
// SessionState is our object that keeps track of a user's session state
|
||||
type SessionState struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
|
||||
RefreshDeadline time.Time `json:"refresh_deadline"`
|
||||
LifetimeDeadline time.Time `json:"lifetime_deadline"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
RefreshDeadline time.Time `json:"refresh_deadline"`
|
||||
|
||||
Email string `json:"email"`
|
||||
User string `json:"user"` // 'sub' in jwt
|
||||
User string `json:"user"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
// LifetimePeriodExpired returns true if the lifetime has expired
|
||||
func (s *SessionState) LifetimePeriodExpired() bool {
|
||||
return isExpired(s.LifetimeDeadline)
|
||||
ImpersonateEmail string
|
||||
ImpersonateGroups []string
|
||||
}
|
||||
|
||||
// RefreshPeriodExpired returns true if the refresh period has expired
|
||||
|
@ -36,6 +36,28 @@ func (s *SessionState) RefreshPeriodExpired() bool {
|
|||
return isExpired(s.RefreshDeadline)
|
||||
}
|
||||
|
||||
type idToken struct {
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Expiry jsonTime `json:"exp"`
|
||||
IssuedAt jsonTime `json:"iat"`
|
||||
Nonce string `json:"nonce"`
|
||||
AtHash string `json:"at_hash"`
|
||||
}
|
||||
|
||||
// IssuedAt parses the IDToken's issue date and returns a valid go time.Time.
|
||||
func (s *SessionState) IssuedAt() (time.Time, error) {
|
||||
payload, err := parseJWT(s.IDToken)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("internal/sessions: malformed jwt: %v", err)
|
||||
}
|
||||
var token idToken
|
||||
if err := json.Unmarshal(payload, &token); err != nil {
|
||||
return time.Time{}, fmt.Errorf("internal/sessions: failed to unmarshal claims: %v", err)
|
||||
}
|
||||
return time.Time(token.IssuedAt), nil
|
||||
}
|
||||
|
||||
func isExpired(t time.Time) bool {
|
||||
return t.Before(time.Now())
|
||||
}
|
||||
|
@ -61,3 +83,37 @@ func UnmarshalSession(value string, c cryptutil.Cipher) (*SessionState, error) {
|
|||
func ExtendDeadline(ttl time.Duration) time.Time {
|
||||
return time.Now().Add(ttl).Truncate(time.Second)
|
||||
}
|
||||
|
||||
func parseJWT(p string) ([]byte, error) {
|
||||
parts := strings.Split(p, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("internal/sessions: malformed jwt, expected 3 parts got %d", len(parts))
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/sessions: malformed jwt payload: %v", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
type jsonTime time.Time
|
||||
|
||||
func (j *jsonTime) UnmarshalJSON(b []byte) error {
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
return err
|
||||
}
|
||||
var unix int64
|
||||
|
||||
if t, err := n.Int64(); err == nil {
|
||||
unix = t
|
||||
} else {
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unix = int64(f)
|
||||
}
|
||||
*j = jsonTime(time.Unix(unix, 0))
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -16,12 +16,11 @@ func TestSessionStateSerialization(t *testing.T) {
|
|||
}
|
||||
|
||||
want := &SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(1 * time.Hour).Truncate(time.Second).UTC(),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}
|
||||
|
||||
ciphertext, err := MarshalSession(want, c)
|
||||
|
@ -43,18 +42,12 @@ func TestSessionStateSerialization(t *testing.T) {
|
|||
|
||||
func TestSessionStateExpirations(t *testing.T) {
|
||||
session := &SessionState{
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
LifetimeDeadline: time.Now().Add(-1 * time.Hour),
|
||||
RefreshDeadline: time.Now().Add(-1 * time.Hour),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
AccessToken: "token1234",
|
||||
RefreshToken: "refresh4321",
|
||||
RefreshDeadline: time.Now().Add(-1 * time.Hour),
|
||||
Email: "user@domain.com",
|
||||
User: "user",
|
||||
}
|
||||
|
||||
if !session.LifetimePeriodExpired() {
|
||||
t.Errorf("expected lifetime period to be expired")
|
||||
}
|
||||
|
||||
if !session.RefreshPeriodExpired() {
|
||||
t.Errorf("expected lifetime period to be expired")
|
||||
}
|
||||
|
@ -80,3 +73,30 @@ func TestExtendDeadline(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionState_IssuedAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
IDToken string
|
||||
want time.Time
|
||||
wantErr bool
|
||||
}{
|
||||
{"simple parse", "eyJhbGciOiJSUzI1NiIsImtpZCI6IjA3YTA4MjgzOWYyZTcxYTliZjZjNTk2OTk2Yjk0NzM5Nzg1YWZkYzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI4NTE4NzcwODIwNTktYmZna3BqMDlub29nN2FzM2dwYzN0N3I2bjlzamJnczYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTE0MzI2NTU5NzcyNzMxNTAzMDgiLCJoZCI6InBvbWVyaXVtLmlvIiwiZW1haWwiOiJiZGRAcG9tZXJpdW0uaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IlkzYm1qV3R4US16OW1fM1RLb0dtRWciLCJuYW1lIjoiQm9iYnkgRGVTaW1vbmUiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1PX1BzRTlILTgzRS9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BQ0hpM3JjQ0U0SFRLVDBhQk1pUFVfOEZfVXFOQ3F6RTBRL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJCb2JieSIsImZhbWlseV9uYW1lIjoiRGVTaW1vbmUiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTU1ODY3MjY4NywiZXhwIjoxNTU4Njc2Mjg3fQ.a4g8W94E7iVJhiIUmsNMwJssfx3Evi8sXeiXgXMC7kHNvftQ2CFU_LJ-dqZ5Jf61OXcrp26r7lUcTNENXuen9tyUWAiHvxk6OHTxZusdywTCY5xowpSZBO9PDWYrmmdvfhRbaKO6QVAUMkbKr1Tr8xqfoaYVXNZhERXhcVReDznI0ccbwCGrNx5oeqiL4eRdZY9eqFXi4Yfee0mkef9oyVPc2HvnpwcpM0eckYa_l_ZQChGjXVGBFIus_Ao33GbWDuc9gs-_Vp2ev4KUT2qWb7AXMCGDLx0tWI9umm7mCBi_7xnaanGKUYcVwcSrv45arllAAwzuNxO0BVw3oRWa5Q", time.Unix(1558672687, 0), false},
|
||||
{"bad jwt", "x.x.x-x-x", time.Time{}, true},
|
||||
{"malformed jwt", "x", time.Time{}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SessionState{IDToken: tt.IDToken}
|
||||
got, err := s.IssuedAt()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SessionState.IssuedAt() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SessionState.IssuedAt() = %v, want %v", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package templates // import "github.com/pomerium/pomerium/internal/templates"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// New loads html and style resources directly. Panics on failure.
|
||||
|
@ -17,129 +14,240 @@ func New() *template.Template {
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-size: 1em;
|
||||
line-height: 1.42857143;
|
||||
color: #333;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
p {
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 40em;
|
||||
display: block;
|
||||
margin: 10% auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,'Helvetica Neue', sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: #F8F8FF;
|
||||
}
|
||||
|
||||
#main {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
.content, .message, button {
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
border-bottom-width: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
#info-box {
|
||||
max-width: 480px;
|
||||
width: 480px;
|
||||
margin-top: 200px;
|
||||
margin-right: auto;
|
||||
margin-bottom: 0px;
|
||||
margin-left: auto;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
color: #32325d;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
text-align: center;
|
||||
background: #F8F8FF;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 15px 0;
|
||||
color: #32325d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0 -30px;
|
||||
padding: 20px 30px 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.content, .message {
|
||||
background-color: #fff;
|
||||
padding: 2rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.error, .message {
|
||||
border-bottom-color: #c00;
|
||||
}
|
||||
.message {
|
||||
padding: 1.5rem 2rem 1.3rem;
|
||||
}
|
||||
header {
|
||||
border-bottom: 1px solid rgba(0,0,0,.075);
|
||||
margin: -2rem 0 2rem;
|
||||
padding: 2rem 0 1.8rem;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
.error header {
|
||||
color: #c00;
|
||||
}
|
||||
.details {
|
||||
font-size: .85rem;
|
||||
color: #999;
|
||||
}
|
||||
button {
|
||||
color: #fff;
|
||||
background-color: #3B8686;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
padding: 1rem 2.5rem;
|
||||
text-shadow: 0 3px 1px rgba(0,0,0,.2);
|
||||
outline: none;
|
||||
}
|
||||
button:active {
|
||||
border-top-width: 4px;
|
||||
border-bottom-width: 1px;
|
||||
text-shadow: none;
|
||||
}
|
||||
footer {
|
||||
font-size: 0.75em;
|
||||
color: #999;
|
||||
border: 1px solid #e8e8fb;
|
||||
background-color: #F8F8FF;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin-bottom: 20px;
|
||||
background: #FCFCFF;
|
||||
box-shadow: 0 1px 3px 0 rgba(50, 50, 93, 0.15), 0 4px 6px 0 rgba(112, 157, 199, 0.15);
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
fieldset label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 42px;
|
||||
padding: 10px 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
fieldset label:not(:last-child) {
|
||||
border-bottom: 1px solid #f0f5fa;
|
||||
}
|
||||
|
||||
fieldset label span {
|
||||
min-width: 125px;
|
||||
padding: 0 15px;
|
||||
text-align: right;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
#group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
#group::before {
|
||||
display: inline-flex;
|
||||
content: '';
|
||||
height: 15px;
|
||||
background-position: -1000px -1000px;
|
||||
background-repeat: no-repeat;
|
||||
// margin-right: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-table;
|
||||
margin-top: -72px;
|
||||
background: #F8F8FF;
|
||||
text-align: center;
|
||||
width: 75px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 115px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.ok{
|
||||
fill: #6E43E8;
|
||||
}
|
||||
|
||||
.error{
|
||||
fill: #EB292F;
|
||||
}
|
||||
|
||||
p.message {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field {
|
||||
flex: 1;
|
||||
padding: 0 15px;
|
||||
background: transparent;
|
||||
font-weight: 400;
|
||||
color: #31325f;
|
||||
outline: none;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
fieldset .select::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
pointer-events: none;
|
||||
background: #6E43E8 url("data:image/svg+xml;utf8,<svg viewBox='0 0 140 140' width='24' height='24' xmlns='http://www.w3.org/2000/svg'><g><path d='m121.3,34.6c-1.6-1.6-4.2-1.6-5.8,0l-51,51.1-51.1-51.1c-1.6-1.6-4.2-1.6-5.8,0-1.6,1.6-1.6,4.2 0,5.8l53.9,53.9c0.8,0.8 1.8,1.2 2.9,1.2 1,0 2.1-0.4 2.9-1.2l53.9-53.9c1.7-1.6 1.7-4.2 0.1-5.8z' fill='white'/></g></svg>") no-repeat;
|
||||
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border-style: none;
|
||||
outline: none;
|
||||
color: #313b3f;
|
||||
}
|
||||
|
||||
select {
|
||||
flex: 1;
|
||||
border-style: none;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
color: #313b3f;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.flex{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #FCFCFF;
|
||||
background: #6E43E8;
|
||||
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 4px;
|
||||
border: 0;
|
||||
font-weight: 700;
|
||||
width: 50%;
|
||||
height: 40px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button.half{
|
||||
flex-grow:0;
|
||||
flex-shrink:0;
|
||||
flex-basis:calc(50% - 10px);
|
||||
}
|
||||
|
||||
.button.full{
|
||||
flex-grow:1;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), 0 3px 6px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.off-color{
|
||||
background: #5735B5;
|
||||
}
|
||||
|
||||
</style>
|
||||
{{end}}`))
|
||||
t = template.Must(t.Parse(fmt.Sprintf(`{{define "footer.html"}}Secured by <b>pomerium</b> %s {{end}}`, version.FullVersion())))
|
||||
|
||||
t = template.Must(t.Parse(`
|
||||
{{define "sign_in_message.html"}}
|
||||
{{if eq (len .AllowedDomains) 1}}
|
||||
{{if eq (index .AllowedDomains 0) "@*"}}
|
||||
<p>You may sign in with any {{.ProviderName}} account.</p>
|
||||
{{else}}
|
||||
<p>You may sign in with your <b>{{index .AllowedDomains 0}}</b> {{.ProviderName}} account.</p>
|
||||
{{end}}
|
||||
{{else if gt (len .AllowedDomains) 1}}
|
||||
<p>
|
||||
You may sign in with any of these {{.ProviderName}} accounts:<br>
|
||||
{{range $i, $e := .AllowedDomains}}{{if $i}}, {{end}}<b>{{$e}}</b>{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
{{end}}`))
|
||||
|
||||
t = template.Must(t.Parse(`
|
||||
{{define "sign_in.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Sign In</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<header>
|
||||
<h1>Sign in to <b>{{.Destination}}</b></h1>
|
||||
</header>
|
||||
|
||||
{{template "sign_in_message.html" .}}
|
||||
|
||||
<form method="GET" action="/start">
|
||||
<input type="hidden" name="redirect_uri" value="{{.Redirect}}">
|
||||
<button type="submit" class="btn">Sign in with {{.ProviderName}}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer>{{template "footer.html"}} </footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}`))
|
||||
|
||||
template.Must(t.Parse(`
|
||||
|
@ -147,51 +255,116 @@ footer {
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Error</title>
|
||||
<title>{{.Code}} - {{.Title}}</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content error">
|
||||
<header>
|
||||
<h1>{{.Title}}</h1>
|
||||
</header>
|
||||
<p>
|
||||
{{.Message}}<br>
|
||||
<span class="details">HTTP {{.Code}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<footer>{{template "footer.html"}} </footer>
|
||||
</div>
|
||||
<div id="main">
|
||||
<div id="info-box">
|
||||
<div class="card">
|
||||
<svg class="icon error" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></svg>
|
||||
<h1 class="title">{{.Title}}</h1>
|
||||
<section>
|
||||
<p class="message">{{.Message}}.</p>
|
||||
<p class="message">Troubleshoot your <a href="/.pomerium">session</a>.</p>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://www.pomerium.io" style="display: block;">
|
||||
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 139 30"><defs><style>.a {fill: #6e43e8;}.a,.b {fill-rule: evenodd;}.b,.c {fill: #fff;}</style></defs><title>powered-by-pomerium</title><path class="a" d="M10.6,5.5H138.4c3.09,0,5.6,2,5.6,4.39V31.11c0,2.42-2.51,4.39-5.6,4.39H10.6c-3.09,0-5.6-2-5.6-4.39V9.89C5,7.47,7.51,5.5,10.6,5.5Z" transform="translate(-5 -5.5)" /><path class="b" d="M75.4,26.62H73.94l1.13-2.79-2.25-5.69h1.54L75.78,22l1.43-3.87h1.54Zm-5.61-2.44a2.42,2.42,0,0,1-1.5-.55V24H66.78V15.56h1.51v3a2.48,2.48,0,0,1,1.5-.55c1.58,0,2.66,1.28,2.66,3.09S71.37,24.18,69.79,24.18Zm-.32-4.88a1.68,1.68,0,0,0-1.18.53v2.52a1.65,1.65,0,0,0,1.18.54c.85,0,1.44-.73,1.44-1.8S70.32,19.3,69.47,19.3Zm-8.8,4.33a2.38,2.38,0,0,1-1.5.55c-1.57,0-2.66-1.27-2.66-3.09S57.6,18,59.17,18a2.44,2.44,0,0,1,1.5.55v-3h1.52V24H60.67Zm0-3.8a1.63,1.63,0,0,0-1.17-.53c-.86,0-1.45.73-1.45,1.79s.59,1.8,1.45,1.8a1.6,1.6,0,0,0,1.17-.54Zm-9,1.68A1.69,1.69,0,0,0,53.47,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,53.11,18,2.66,2.66,0,0,1,55.7,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34a1.38,1.38,0,0,0-1.37,1.36h2.57A1.28,1.28,0,0,0,53.05,19.17Zm-5.34.93V24H46.2v-5.9h1.51v.59A2,2,0,0,1,49.16,18a1.65,1.65,0,0,1,.49.06v1.35a1.83,1.83,0,0,0-.53-.07A1.87,1.87,0,0,0,47.71,20.1ZM41,21.51A1.69,1.69,0,0,0,42.76,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,42.4,18,2.66,2.66,0,0,1,45,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34A1.38,1.38,0,0,0,41,20.53h2.57A1.28,1.28,0,0,0,42.34,19.17ZM35.7,24l-1.2-4-1.2,4H32l-2-5.9h1.51l1.19,4,1.19-4h1.37l1.19,4,1.19-4h1.51l-2,5.9Zm-9.23.14a2.94,2.94,0,0,1-3-3.09,3,3,0,1,1,6.07,0A2.94,2.94,0,0,1,26.47,24.18Zm0-4.92c-.88,0-1.49.75-1.49,1.83s.61,1.83,1.49,1.83S28,22.18,28,21.09,27.35,19.26,26.47,19.26Zm-6.62,1.87H18.49V24H17V15.93h2.87a2.61,2.61,0,1,1,0,5.2Zm-.22-4H18.49V19.9h1.14a1.38,1.38,0,1,0,0-2.75Z" transform="translate(-5 -5.5)" /><path class="c" d="M132.71,14.9A3.93,3.93,0,0,0,128.78,11H94.59a3.93,3.93,0,0,0-3.93,3.92V31.06h2.71V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2.47ZM93.37,19a5.49,5.49,0,1,1,11,0Zm12.95,0a5.49,5.49,0,1,1,11,0Zm12.94,0a5.49,5.49,0,1,1,11,0Z" transform="translate(-5 -5.5)" /></svg>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>{{end}}`))
|
||||
</html>
|
||||
|
||||
{{end}}`))
|
||||
|
||||
t = template.Must(t.Parse(`
|
||||
{{define "sign_out.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Sign Out</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<header>
|
||||
<h1>Sign out of <b>{{.Destination}}</b></h1>
|
||||
</header>
|
||||
|
||||
<p>You're currently signed in as <b>{{.Email}}</b>. This will also sign you out of other internal apps.</p>
|
||||
<form method="POST" action="/sign_out">
|
||||
<input type="hidden" name="redirect_uri" value="{{.Redirect}}">
|
||||
<input type="hidden" name="sig" value="{{.Signature}}">
|
||||
<input type="hidden" name="ts" value="{{.Timestamp}}">
|
||||
<button type="submit">Sign out</button>
|
||||
</form>
|
||||
</div>
|
||||
<footer>{{template "footer.html"}}</footer>
|
||||
</div>
|
||||
</body>
|
||||
{{define "dashboard.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
|
||||
<head>
|
||||
<title>Pomerium</title>
|
||||
{{template "header.html"}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main">
|
||||
<div id="info-box">
|
||||
<div class="card">
|
||||
<svg class="icon ok" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
||||
</svg>
|
||||
<form method="POST" action="{{.SignoutURL}}">
|
||||
<section>
|
||||
<h2>Session</h2>
|
||||
<p class="message">Your current session details.</p>
|
||||
<fieldset>
|
||||
<label>
|
||||
<span>Email</span>
|
||||
<input name="email" type="email" class="field" value="{{.Email}}" disabled>
|
||||
</label>
|
||||
<label>
|
||||
<span>User</span>
|
||||
<input name="user" type="text" class="field" value="{{.User}}" disabled>
|
||||
</label>
|
||||
<label class="select">
|
||||
<span>Groups</span>
|
||||
<div id="group" class="field">
|
||||
<select name="group">
|
||||
{{range .Groups}}
|
||||
<option value="{{.}}">{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>Expiry</span>
|
||||
<input name="session expiration" type="text" class="field" value="{{.RefreshDeadline}}" disabled>
|
||||
</label>
|
||||
</fieldset>
|
||||
</section>
|
||||
<div class="flex">
|
||||
<button class="button half" type="submit">Sign Out</button>
|
||||
<a href="/.pomerium/refresh" class="button half">Refresh</a>
|
||||
</div>
|
||||
</form>
|
||||
{{if .IsAdmin}}
|
||||
<form method="POST" action="/.pomerium/impersonate">
|
||||
<section>
|
||||
<h2>Sign-in-as</h2>
|
||||
<p class="message">Administrators can temporarily impersonate another a user.</p>
|
||||
<fieldset>
|
||||
<label>
|
||||
<span>Email</span>
|
||||
<input name="email" type="email" class="field" value="{{.ImpersonateEmail}}" placeholder="user@example.com">
|
||||
</label>
|
||||
<label>
|
||||
<span>Group</span>
|
||||
<input name="group" type="text" class="field" value="{{.ImpersonateGroup}}" placeholder="engineering">
|
||||
</label>
|
||||
</fieldset>
|
||||
</section>
|
||||
<div class="flex">
|
||||
<input name="csrf" type="hidden" value="{{.CSRF}}">
|
||||
<button class="button full" type="submit">Impersonate session</button>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://www.pomerium.io" style="display: block;">
|
||||
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 139 30"><defs><style>.a {fill: #6e43e8;}.a,.b {fill-rule: evenodd;}.b,.c {fill: #fff;}</style></defs><title>powered-by-pomerium</title><path class="a" d="M10.6,5.5H138.4c3.09,0,5.6,2,5.6,4.39V31.11c0,2.42-2.51,4.39-5.6,4.39H10.6c-3.09,0-5.6-2-5.6-4.39V9.89C5,7.47,7.51,5.5,10.6,5.5Z" transform="translate(-5 -5.5)" /><path class="b" d="M75.4,26.62H73.94l1.13-2.79-2.25-5.69h1.54L75.78,22l1.43-3.87h1.54Zm-5.61-2.44a2.42,2.42,0,0,1-1.5-.55V24H66.78V15.56h1.51v3a2.48,2.48,0,0,1,1.5-.55c1.58,0,2.66,1.28,2.66,3.09S71.37,24.18,69.79,24.18Zm-.32-4.88a1.68,1.68,0,0,0-1.18.53v2.52a1.65,1.65,0,0,0,1.18.54c.85,0,1.44-.73,1.44-1.8S70.32,19.3,69.47,19.3Zm-8.8,4.33a2.38,2.38,0,0,1-1.5.55c-1.57,0-2.66-1.27-2.66-3.09S57.6,18,59.17,18a2.44,2.44,0,0,1,1.5.55v-3h1.52V24H60.67Zm0-3.8a1.63,1.63,0,0,0-1.17-.53c-.86,0-1.45.73-1.45,1.79s.59,1.8,1.45,1.8a1.6,1.6,0,0,0,1.17-.54Zm-9,1.68A1.69,1.69,0,0,0,53.47,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,53.11,18,2.66,2.66,0,0,1,55.7,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34a1.38,1.38,0,0,0-1.37,1.36h2.57A1.28,1.28,0,0,0,53.05,19.17Zm-5.34.93V24H46.2v-5.9h1.51v.59A2,2,0,0,1,49.16,18a1.65,1.65,0,0,1,.49.06v1.35a1.83,1.83,0,0,0-.53-.07A1.87,1.87,0,0,0,47.71,20.1ZM41,21.51A1.69,1.69,0,0,0,42.76,23a3.55,3.55,0,0,0,1.76-.56v1.26a4.73,4.73,0,0,1-2,.46,3,3,0,0,1-3-3.13A2.87,2.87,0,0,1,42.4,18,2.66,2.66,0,0,1,45,21a5.53,5.53,0,0,1,0,.56Zm1.37-2.34A1.38,1.38,0,0,0,41,20.53h2.57A1.28,1.28,0,0,0,42.34,19.17ZM35.7,24l-1.2-4-1.2,4H32l-2-5.9h1.51l1.19,4,1.19-4h1.37l1.19,4,1.19-4h1.51l-2,5.9Zm-9.23.14a2.94,2.94,0,0,1-3-3.09,3,3,0,1,1,6.07,0A2.94,2.94,0,0,1,26.47,24.18Zm0-4.92c-.88,0-1.49.75-1.49,1.83s.61,1.83,1.49,1.83S28,22.18,28,21.09,27.35,19.26,26.47,19.26Zm-6.62,1.87H18.49V24H17V15.93h2.87a2.61,2.61,0,1,1,0,5.2Zm-.22-4H18.49V19.9h1.14a1.38,1.38,0,1,0,0-2.75Z" transform="translate(-5 -5.5)" /><path class="c" d="M132.71,14.9A3.93,3.93,0,0,0,128.78,11H94.59a3.93,3.93,0,0,0-3.93,3.92V31.06h2.71V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2V26.55h0a5.49,5.49,0,1,1,11,0h0v4.51h2.47ZM93.37,19a5.49,5.49,0,1,1,11,0Zm12.95,0a5.49,5.49,0,1,1,11,0Zm12.94,0a5.49,5.49,0,1,1,11,0Z" transform="translate(-5 -5.5)" /></svg>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}`))
|
||||
return t
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue