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:
Caleb Doxsey 2022-02-07 08:47:58 -07:00 committed by GitHub
parent 64d8748251
commit 2824faecbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 13373 additions and 1455 deletions

View file

@ -1,2 +1,5 @@
dist/ dist/
bin/ bin/
ui/dist/index.js
ui/dist/index.css
ui/node_modules/

View file

@ -10,12 +10,18 @@ jobs:
strategy: strategy:
matrix: matrix:
go-version: [1.17.x] go-version: [1.17.x]
node-version: [16.x]
platform: [ubuntu-latest, macos-latest] platform: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: set env vars - name: set env vars
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -50,10 +56,18 @@ jobs:
cover: cover:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.17.x]
node-version: [16.x]
steps: steps:
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: 1.17.x go-version: ${{ matrix.go-version }}
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
@ -99,6 +113,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
go-version: [1.17.x] go-version: [1.17.x]
node-version: [16.x]
platform: [ubuntu-latest] platform: [ubuntu-latest]
deployment: [kubernetes, multi, nginx, single, traefik] deployment: [kubernetes, multi, nginx, single, traefik]
idp: [auth0, azure, github, gitlab, google, oidc, okta, onelogin, ping] idp: [auth0, azure, github, gitlab, google, oidc, okta, onelogin, ping]
@ -107,6 +122,11 @@ jobs:
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: set env vars - name: set env vars
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -141,12 +161,18 @@ jobs:
strategy: strategy:
matrix: matrix:
go-version: [1.17.x] go-version: [1.17.x]
node-version: [16.x]
platform: [ubuntu-latest, macos-latest] platform: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0

7
.gitignore vendored
View file

@ -62,6 +62,8 @@ tags
# dependencies # dependencies
ui/node_modules ui/node_modules
ui/bower_components ui/bower_components
ui/dist/index.js
ui/dist/index.css
# for building static assets # for building static assets
node_modules node_modules
@ -74,7 +76,6 @@ lib/core/MetadataBlog.js
translated_docs translated_docs
build/ build/
yarn.lock
node_modules node_modules
i18n/* i18n/*
docs/.vuepress/dist/ docs/.vuepress/dist/
@ -91,4 +92,6 @@ docs/.vuepress/dist/
/bazel-* /bazel-*
internal/envoy/files/envoy-*-????? internal/envoy/files/envoy-*-?????
internal/envoy/files/envoy-*-?????.sha256 internal/envoy/files/envoy-*-?????.sha256
internal/envoy/files/envoy-*-?????.version internal/envoy/files/envoy-*-?????.version
yarn-error.log

View file

@ -1,4 +1,19 @@
FROM golang:latest as build FROM node:16 as ui
WORKDIR /build
COPY .git ./.git
COPY Makefile ./Makefile
# download yarn dependencies
COPY ui/yarn.lock ./ui/yarn.lock
COPY ui/package.json ./ui/package.json
RUN make yarn
# build ui
COPY ./ui/ ./ui/
RUN make build-ui
FROM golang:1.17-buster as build
WORKDIR /go/src/github.com/pomerium/pomerium WORKDIR /go/src/github.com/pomerium/pomerium
RUN apt-get update \ RUN apt-get update \
@ -8,10 +23,10 @@ RUN apt-get update \
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
COPY --from=ui /build/ui/dist ./ui/dist
# build # build
RUN make build-deps RUN make build-go NAME=pomerium
RUN make build NAME=pomerium
RUN touch /config.yaml RUN touch /config.yaml
# build our own root trust store from current stable # build our own root trust store from current stable

View file

@ -71,25 +71,29 @@ tag: ## Create a new git tag to prepare to build a release
git tag -sa $(VERSION) -m "$(VERSION)" git tag -sa $(VERSION) -m "$(VERSION)"
@echo "Run git push origin $(VERSION) to push your new tag to GitHub." @echo "Run git push origin $(VERSION) to push your new tag to GitHub."
.PHONY: frontend
frontend: ## Runs go generate on the static assets package.
@echo "==> $@"
@CGO_ENABLED=0 GO111MODULE=on $(GO) generate github.com/pomerium/pomerium/internal/frontend
.PHONY: proto .PHONY: proto
proto: proto:
@echo "==> $@" @echo "==> $@"
cd pkg/grpc && ./protoc.bash cd pkg/grpc && ./protoc.bash
.PHONY: build .PHONY: build
build: build-deps ## Builds dynamic executables and/or packages. build: build-go build-ui
@echo "==> $@"
.PHONY: build-debug
build-debug: build-deps build-ui ## Builds binaries appropriate for debugging
@echo "==> $@"
@CGO_ENABLED=0 GO111MODULE=on $(GO) build -gcflags="all=-N -l" -o $(BINDIR)/$(NAME) ./cmd/"$(NAME)"
.PHONY: build-go
build-go: build-deps
@echo "==> $@" @echo "==> $@"
@CGO_ENABLED=0 GO111MODULE=on $(GO) build -tags "$(BUILDTAGS)" ${GO_LDFLAGS} -o $(BINDIR)/$(NAME) ./cmd/"$(NAME)" @CGO_ENABLED=0 GO111MODULE=on $(GO) build -tags "$(BUILDTAGS)" ${GO_LDFLAGS} -o $(BINDIR)/$(NAME) ./cmd/"$(NAME)"
.PHONY: build-debug .PHONY: build-ui
build-debug: build-deps ## Builds binaries appropriate for debugging build-ui: yarn
@echo "==> $@" @echo "==> $@"
@CGO_ENABLED=0 GO111MODULE=on $(GO) build -gcflags="all=-N -l" -o $(BINDIR)/$(NAME) ./cmd/"$(NAME)" @cd ui; yarn build
.PHONY: lint .PHONY: lint
lint: ## Verifies `golint` passes. lint: ## Verifies `golint` passes.
@ -129,6 +133,11 @@ snapshot: build-deps ## Builds the cross-compiled binaries, naming them in such
@echo "==> $@" @echo "==> $@"
@goreleaser release --rm-dist -f .github/goreleaser.yaml --snapshot @goreleaser release --rm-dist -f .github/goreleaser.yaml --snapshot
.PHONY: yarn
yarn:
@echo "==> $@"
cd ui ; yarn install
.PHONY: help .PHONY: help
help: help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View file

@ -6,11 +6,8 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/url"
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/identity" "github.com/pomerium/pomerium/internal/identity"
"github.com/pomerium/pomerium/internal/identity/oauth" "github.com/pomerium/pomerium/internal/identity/oauth"
"github.com/pomerium/pomerium/internal/log" "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. // Authenticate contains data required to run the authenticate service.
type Authenticate struct { type Authenticate struct {
templates *template.Template
options *config.AtomicOptions options *config.AtomicOptions
provider *identity.AtomicAuthenticator provider *identity.AtomicAuthenticator
state *atomicAuthenticateState state *atomicAuthenticateState
@ -58,10 +53,9 @@ type Authenticate struct {
// New validates and creates a new authenticate service from a set of Options. // New validates and creates a new authenticate service from a set of Options.
func New(cfg *config.Config) (*Authenticate, error) { func New(cfg *config.Config) (*Authenticate, error) {
a := &Authenticate{ a := &Authenticate{
templates: template.Must(frontend.NewTemplates()), options: config.NewAtomicOptions(),
options: config.NewAtomicOptions(), provider: identity.NewAtomicAuthenticator(),
provider: identity.NewAtomicAuthenticator(), state: newAtomicAuthenticateState(newAuthenticateState()),
state: newAtomicAuthenticateState(newAuthenticateState()),
} }
state, err := newAuthenticateStateFromConfig(cfg) state, err := newAuthenticateStateFromConfig(cfg)
@ -123,17 +117,3 @@ func (a *Authenticate) updateProvider(cfg *config.Config) error {
return nil 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
}

View file

@ -33,6 +33,7 @@ import (
"github.com/pomerium/pomerium/pkg/grpc/directory" "github.com/pomerium/pomerium/pkg/grpc/directory"
"github.com/pomerium/pomerium/pkg/grpc/session" "github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user" "github.com/pomerium/pomerium/pkg/grpc/user"
"github.com/pomerium/pomerium/ui"
) )
// Handler returns the authenticate service's handler chain. // 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_in").Handler(a.requireValidSignature(a.SignIn))
sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut)) sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut))
sr.Path("/webauthn").Handler(webauthn.New(a.getWebauthnState)) 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 := sr.PathPrefix("/callback").Subrouter()
cr.Use(func(h http.Handler) http.Handler { 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() state := a.state.Load()
authenticateURL, err := a.options.Load().GetAuthenticateURL()
if err != nil {
return err
}
s, err := a.getSessionFromCtx(ctx) s, err := a.getSessionFromCtx(ctx)
if err != nil { if err != nil {
s.ID = uuid.New().String() s.ID = uuid.New().String()
@ -500,52 +526,17 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
groups = append(groups, pbDirectoryGroup) groups = append(groups, pbDirectoryGroup)
} }
signoutURL, err := a.getSignOutURL(r) handlers.UserInfo(handlers.UserInfoData{
if err != nil { CSRFToken: csrf.Token(r),
return fmt.Errorf("invalid signout url: %w", err) DirectoryGroups: groups,
} DirectoryUser: pbDirectoryUser,
IsImpersonated: isImpersonated,
webAuthnURL, err := a.getWebAuthnURL(r.URL.Query()) Session: pbSession,
if err != nil { SignOutURL: urlutil.SignOutURL(r, authenticateURL, state.sharedKey),
return fmt.Errorf("invalid webauthn url: %w", err) User: pbUser,
} WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()),
}).ServeHTTP(w, r)
type DeviceCredentialInfo struct { return nil
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)
} }
func (a *Authenticate) saveSessionToDataBroker( func (a *Authenticate) saveSessionToDataBroker(
@ -682,12 +673,18 @@ func (a *Authenticate) getWebauthnState(ctx context.Context) (*webauthn.State, e
return nil, err return nil, err
} }
authenticateURL, err := a.options.Load().GetAuthenticateURL()
if err != nil {
return nil, err
}
pomeriumDomains, err := a.options.Load().GetAllRouteableHTTPDomains() pomeriumDomains, err := a.options.Load().GetAllRouteableHTTPDomains()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &webauthn.State{ return &webauthn.State{
AuthenticateURL: authenticateURL,
SharedKey: state.sharedKey, SharedKey: state.sharedKey,
Client: state.dataBrokerClient, Client: state.dataBrokerClient,
PomeriumDomains: pomeriumDomains, PomeriumDomains: pomeriumDomains,

View file

@ -1,18 +1,19 @@
package handlers package handlers
import ( import (
"html/template"
"net/http" "net/http"
"net/url"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/httputil" "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. // DeviceEnrolled displays an HTML page informing the user that they've successfully enrolled a device.
func DeviceEnrolled() http.Handler { func DeviceEnrolled(authenticateURL *url.URL, sharedKey []byte) http.Handler {
tpl := template.Must(frontend.NewTemplates())
type TemplateData struct{}
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { 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),
})
}) })
} }

View 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())
})
}

View file

@ -8,21 +8,17 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"html/template"
"io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pomerium/csrf"
"github.com/pomerium/webauthn" "github.com/pomerium/webauthn"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/pomerium/pomerium/internal/encoding/jws" "github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/middleware" "github.com/pomerium/pomerium/internal/middleware"
"github.com/pomerium/pomerium/internal/sessions" "github.com/pomerium/pomerium/internal/sessions"
@ -33,6 +29,7 @@ import (
"github.com/pomerium/pomerium/pkg/grpc/session" "github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user" "github.com/pomerium/pomerium/pkg/grpc/user"
"github.com/pomerium/pomerium/pkg/webauthnutil" "github.com/pomerium/pomerium/pkg/webauthnutil"
"github.com/pomerium/pomerium/ui"
) )
const maxAuthenticateResponses = 5 const maxAuthenticateResponses = 5
@ -50,13 +47,14 @@ var (
// State is the state needed by the Handler to handle requests. // State is the state needed by the Handler to handle requests.
type State struct { type State struct {
SharedKey []byte AuthenticateURL *url.URL
Client databroker.DataBrokerServiceClient Client databroker.DataBrokerServiceClient
PomeriumDomains []string PomeriumDomains []string
RelyingParty *webauthn.RelyingParty
Session *session.Session Session *session.Session
SessionState *sessions.State SessionState *sessions.State
SessionStore sessions.SessionStore SessionStore sessions.SessionStore
RelyingParty *webauthn.RelyingParty SharedKey []byte
} }
// A StateProvider provides state for the handler. // A StateProvider provides state for the handler.
@ -64,15 +62,13 @@ type StateProvider = func(context.Context) (*State, error)
// Handler is the WebAuthn device handler. // Handler is the WebAuthn device handler.
type Handler struct { type Handler struct {
getState StateProvider getState StateProvider
templates *template.Template
} }
// New creates a new Handler. // New creates a new Handler.
func New(getState StateProvider) *Handler { func New(getState StateProvider) *Handler {
return &Handler{ return &Handler{
getState: getState, getState: getState,
templates: template.Must(frontend.NewTemplates()),
} }
} }
@ -373,23 +369,12 @@ func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *Stat
creationOptions := webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u) creationOptions := webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u)
requestOptions := webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials) requestOptions := webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials)
var buf bytes.Buffer return ui.ServePage(w, r, "WebAuthnRegistration", map[string]interface{}{
err = h.templates.ExecuteTemplate(&buf, "webauthn.html", map[string]interface{}{ "creationOptions": creationOptions,
"csrfField": csrf.TemplateField(r), "requestOptions": requestOptions,
"Data": map[string]interface{}{ "selfUrl": r.URL.String(),
"creationOptions": creationOptions, "signOutUrl": urlutil.SignOutURL(r, state.AuthenticateURL, state.SharedKey),
"requestOptions": requestOptions,
},
"SelfURL": r.URL.String(),
}) })
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 { func (h *Handler) saveSessionAndRedirect(w http.ResponseWriter, r *http.Request, state *State, rawRedirectURI string) error {

View file

@ -5,7 +5,6 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@ -29,7 +28,6 @@ import (
"github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/jws" "github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/encoding/mock" "github.com/pomerium/pomerium/internal/encoding/mock"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/identity" "github.com/pomerium/pomerium/internal/identity"
"github.com/pomerium/pomerium/internal/identity/oidc" "github.com/pomerium/pomerium/internal/identity/oidc"
@ -49,7 +47,6 @@ func testAuthenticate() *Authenticate {
redirectURL: redirectURL, redirectURL: redirectURL,
cookieSecret: cryptutil.NewKey(), cookieSecret: cryptutil.NewKey(),
}) })
auth.templates = template.Must(frontend.NewTemplates())
auth.options = config.NewAtomicOptions() auth.options = config.NewAtomicOptions()
auth.options.Store(&config.Options{ auth.options.Store(&config.Options{
SharedKey: cryptutil.NewBase64Key(), SharedKey: cryptutil.NewBase64Key(),
@ -268,9 +265,8 @@ func TestAuthenticate_SignOut(t *testing.T) {
}, },
directoryClient: new(mockDirectoryServiceClient), directoryClient: new(mockDirectoryServiceClient),
}), }),
templates: template.Must(frontend.NewTemplates()), options: config.NewAtomicOptions(),
options: config.NewAtomicOptions(), provider: identity.NewAtomicAuthenticator(),
provider: identity.NewAtomicAuthenticator(),
} }
if tt.signoutRedirectURL != "" { if tt.signoutRedirectURL != "" {
opts := a.options.Load() opts := a.options.Load()
@ -671,7 +667,6 @@ func TestAuthenticate_userInfo(t *testing.T) {
}, },
directoryClient: new(mockDirectoryServiceClient), directoryClient: new(mockDirectoryServiceClient),
}), }),
templates: template.Must(frontend.NewTemplates()),
} }
r := httptest.NewRequest(tt.method, tt.url.String(), nil) r := httptest.NewRequest(tt.method, tt.url.String(), nil)
state, err := tt.sessionStore.LoadSession(r) state, err := tt.sessionStore.LoadSession(r)
@ -761,7 +756,6 @@ func TestAuthenticate_SignOut_CSRF(t *testing.T) {
}, },
directoryClient: new(mockDirectoryServiceClient), directoryClient: new(mockDirectoryServiceClient),
}), }),
templates: template.Must(frontend.NewTemplates()),
} }
tests := []struct { tests := []struct {
name string name string

View file

@ -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
}

View file

@ -5,12 +5,10 @@ package authorize
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"sync" "sync"
"github.com/pomerium/pomerium/authorize/evaluator" "github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry/metrics" "github.com/pomerium/pomerium/internal/telemetry/metrics"
"github.com/pomerium/pomerium/internal/telemetry/trace" "github.com/pomerium/pomerium/internal/telemetry/trace"
@ -22,7 +20,6 @@ type Authorize struct {
state *atomicAuthorizeState state *atomicAuthorizeState
store *evaluator.Store store *evaluator.Store
currentOptions *config.AtomicOptions currentOptions *config.AtomicOptions
templates *template.Template
dataBrokerInitialSync chan struct{} dataBrokerInitialSync chan struct{}
@ -37,7 +34,6 @@ func New(cfg *config.Config) (*Authorize, error) {
a := Authorize{ a := Authorize{
currentOptions: config.NewAtomicOptions(), currentOptions: config.NewAtomicOptions(),
store: evaluator.NewStore(), store: evaluator.NewStore(),
templates: template.Must(frontend.NewTemplates()),
dataBrokerInitialSync: make(chan struct{}), dataBrokerInitialSync: make(chan struct{}),
} }

View file

@ -2,7 +2,6 @@ package authorize
import ( import (
"context" "context"
"html/template"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
@ -19,7 +18,6 @@ import (
"github.com/pomerium/pomerium/authorize/evaluator" "github.com/pomerium/pomerium/authorize/evaluator"
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/encoding/jws" "github.com/pomerium/pomerium/internal/encoding/jws"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/testutil" "github.com/pomerium/pomerium/internal/testutil"
"github.com/pomerium/pomerium/pkg/grpc/session" "github.com/pomerium/pomerium/pkg/grpc/session"
"github.com/pomerium/pomerium/pkg/grpc/user" "github.com/pomerium/pomerium/pkg/grpc/user"
@ -114,7 +112,6 @@ func TestAuthorize_deniedResponse(t *testing.T) {
}}, }},
}}, }},
}) })
a.templates = template.Must(frontend.NewTemplates())
tests := []struct { tests := []struct {
name string name string
@ -138,7 +135,6 @@ func TestAuthorize_deniedResponse(t *testing.T) {
Code: envoy_type_v3.StatusCode(codes.InvalidArgument), Code: envoy_type_v3.StatusCode(codes.InvalidArgument),
}, },
Headers: []*envoy_config_core_v3.HeaderValueOption{ Headers: []*envoy_config_core_v3.HeaderValueOption{
mkHeader("Content-Type", "text/html; charset=UTF-8", false),
mkHeader("X-Pomerium-Intercepted-Response", "true", false), mkHeader("X-Pomerium-Intercepted-Response", "true", false),
}, },
Body: "Access Denied", Body: "Access Denied",
@ -155,7 +151,7 @@ func TestAuthorize_deniedResponse(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.want.Status.Code, got.Status.Code) assert.Equal(t, tc.want.Status.Code, got.Status.Code)
assert.Equal(t, tc.want.Status.Message, got.Status.Message) assert.Equal(t, tc.want.Status.Message, got.Status.Message)
assert.Equal(t, tc.want.GetDeniedResponse().GetHeaders(), got.GetDeniedResponse().GetHeaders()) testutil.AssertProtoEqual(t, tc.want.GetDeniedResponse().GetHeaders(), got.GetDeniedResponse().GetHeaders())
}) })
} }
} }

View file

@ -45,21 +45,6 @@ func TestDashboard(t *testing.T) {
assert.Equal(t, 3, res.StatusCode/100, "unexpected status code") assert.Equal(t, 3, res.StatusCode/100, "unexpected status code")
}) })
t.Run("image asset", func(t *testing.T) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://authenticate.localhost.pomerium.io/.pomerium/assets/img/pomerium.svg", nil)
if err != nil {
t.Fatal(err)
}
res, err := getClient().Do(req)
if !assert.NoError(t, err, "unexpected http error") {
return
}
defer res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code")
assert.Equal(t, "image/svg+xml", res.Header.Get("Content-Type"))
})
} }
func TestHealth(t *testing.T) { func TestHealth(t *testing.T) {

View file

@ -9,7 +9,6 @@ import (
"github.com/CAFxX/httpcompression" "github.com/CAFxX/httpcompression"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry" "github.com/pomerium/pomerium/internal/telemetry"
@ -48,7 +47,6 @@ func (srv *Server) addHTTPMiddleware() {
}, srv.name)) }, srv.name))
root.HandleFunc("/healthz", httputil.HealthCheck) root.HandleFunc("/healthz", httputil.HealthCheck)
root.HandleFunc("/ping", httputil.HealthCheck) root.HandleFunc("/ping", httputil.HealthCheck)
root.PathPrefix("/.pomerium/assets/").Handler(http.StripPrefix("/.pomerium/assets/", frontend.MustAssetHandler()))
// pprof // pprof
root.Path("/debug/pprof/cmdline").HandlerFunc(pprof.Cmdline) root.Path("/debug/pprof/cmdline").HandlerFunc(pprof.Cmdline)

View file

@ -1,30 +0,0 @@
{{define "device-enrolled.html"}}<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>Device Successfully Enrolled</title>
{{template "header.html"}}
</head>
<body>
<div class="inner">
<div class="header clearfix">
<div class="heading"></div>
</div>
<div class="content">
<div class="white box">
<div class="largestatus">
<div class="title-wrapper">
<span class="title">Device Successfully Enrolled</span>
<label class="status-time">
<span>
Device was successfully enrolled.
</span>
</label>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
{{end}}

View file

@ -1,45 +0,0 @@
{{define "error.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>{{.Status}} - {{.StatusText}}</title>
{{template "header.html"}}
</head>
<body>
<div class="inner">
<div class="header clearfix">
<div class="heading"></div>
</div>
<div class="content">
<div class="white box">
<div class="largestatus">
<img class="status-bubble" src="{{dataURL "/.pomerium/assets/img/error-24px.svg"}}" xmlns="http://www.w3.org/2000/svg" />
<div class="title-wrapper">
<span class="title">{{.Status}} {{.StatusText}}</span>
<label class="status-time">
<span>{{.Error}}</span>
</label>
</div>
</div>
<div class="category-link">
{{if .CanDebug}}
If you should have access, contact your administrator with your
{{if .RequestID }}
request id {{.RequestID}}
{{end}}
{{if and .RequestID .DebugURL }}
and
{{end}}
{{if .DebugURL }}
<a href="{{.DebugURL}}">session details</a>.
{{end}}
{{end}}
</div>
</div>
</div>
</div>
</body>
</html>
{{end}}

View file

@ -1,5 +0,0 @@
{{define "header.html"}}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link rel="stylesheet" type="text/css" href="{{dataURL "/.pomerium/assets/style/main.css"}}"/>
<link rel="icon" type="image/png" href="{{dataURL "/.pomerium/assets/img/logo-only.svg"}}" />
{{end}}

View file

@ -1,276 +0,0 @@
{{define "userInfo.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>User info endpoint</title>
{{template "header.html"}}
</head>
<body>
<div class="inner">
<div class="header clearfix">
<div class="heading">
<a href="{{.RedirectURL}}" class="logo"></a>
<span>
<form action="{{.SignOutURL}}" method="post">
{{.csrfField}}
<input class="button" type="submit" value="Logout"/>
</form>
</span>
</div>
</div>
<div class="content">
<div class="white box">
<div class="largestatus">
{{range .User.GetClaim "picture"}}
<img class="status-bubble" src="{{.|safeURL}}" alt="user image" />
{{else}}
<img class="status-bubble" src="{{dataURL "/.pomerium/assets/img/account_circle-24px.svg"}}" xmlns="http://www.w3.org/2000/svg" />
{{end}}
<div class="title-wrapper">
<span class="title">
{{with .User.Name}}
Hi {{.}}!
{{else}}
{{range .User.GetClaim "given_name"}}
Hi {{.}}!
{{end}}
{{end}}
</span>
<label class="status-time">
<span>
Welcome to the user info endpoint. Here you can view
your current session details, and authorization context.
</span>
</label>
</div>
</div>
</div>
<div class="category white box">
<div class="messages">
<div class="box-inner">
<div class="category-header clearfix">
<span class="category-title">Session Details</span>
</div>
{{if .Session}}
<table>
<thead>
<tr>
<th><a href="https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Claims</a></th>
<th></th>
</tr>
</thead>
<tbody>
{{with .Session.UserId}}
<tr>
<td>User ID</td>
<td>{{.}}</td>
</tr>
{{end}}
{{with .Session.Id}}
<tr>
<td>ID</td>
<td>{{.}}</td>
</tr>
{{end}}
{{with .Session.ExpiresAt}}
<tr>
<td>Expires At</td>
<td>{{.AsTime | formatTime}}</td>
</tr>
{{end}}
<tr>
<td>Impersonated</td>
<td>{{.IsImpersonated}}</td>
</tr>
</tbody>
</table>
{{else}}
No session details found!
{{end}}
</div>
</div>
</div>
<div class="category white box">
<div class="messages">
<div class="box-inner">
<div class="category-header clearfix">
<span class="category-title">User Claims</span>
{{with .Session.IdToken}}
<a href="https://jwt.io/#debugger-io?token={{.Raw}}">
<span class="category-icon"> </span>
</a>
{{end}}
</div>
{{if .Session}}
<table>
<thead>
<tr>
<th><a href="https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Claims</a></th>
<th></th>
</tr>
</thead>
<tbody>
{{range $k,$v:=.Session.Claims}}
<tr>
<td>{{$k}}</td>
<td>
{{range $v.AsSlice}}
{{if eq $k "exp" "iat" "updated_at"}}
<p>{{formatTime .}}</p>
{{else}}
<p>{{.}}</p>
{{end}}
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
No user claims found!
{{end}}
</div>
<div class="category-link">
User <a href="https://docs.pomerium.io/reference/#jwt-claim-headers">identity claims</a> can be passed to upstream applications.
</div>
</div>
</div>
<div class="category white box">
<div class="messages">
<div class="box-inner">
<div class="category-header clearfix">
<span class="category-title">Groups</span>
</div>
{{if .DirectoryGroups}}
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
{{range .DirectoryGroups}}
<td>{{.Id}}</td>
<td> {{.Name}} </td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
No groups found!
{{end}}
</div>
<div class="category-link">
Your associated groups are pulled from your <a href="https://www.pomerium.io/docs/identity-providers/">identity provider</a>.
</div>
</div>
</div>
<div class="category white box">
<div class="messages">
<div class="box-inner">
<div class="category-header clearfix">
<span class="category-title">Current Session Device Credentials</span>
<span class="experimental-icon"> </span>
</div>
{{if .CurrentDeviceCredentials}}
<table>
<thead>
<tr>
<th>ID</th>
</tr>
</thead>
<tbody>
{{range .CurrentDeviceCredentials}}
<tr>
<td>{{.ID}}</td>
<td>
<form action="{{$.WebAuthnURL}}" method="POST" class="delete-credential-form">
{{$.csrfField}}
<input type="hidden" name="action" value="unregister">
<input type="hidden" name="pomerium_device_credential_id" value="{{.ID}}">
<button type="submit">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
No device credentials found!
{{end}}
</div>
{{if .OtherDeviceCredentials}}
<div class="box-inner">
<div class="category-header clearfix">
<span class="category-title">Other Device Credentials</span>
</div>
<table>
<thead>
<tr>
<th>ID</th>
</tr>
</thead>
<tbody>
{{range .OtherDeviceCredentials}}
<tr>
<td>{{.ID}}</td>
<td>
<form action="{{$.WebAuthnURL}}" method="POST" class="delete-credential-form">
{{$.csrfField}}
<input type="hidden" name="action" value="unregister">
<input type="hidden" name="pomerium_device_credential_id" value="{{.ID}}">
<button type="submit">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<div class="category-link">
Register device with <a href="{{.WebAuthnURL}}">WebAuthn</a>.
</div>
</div>
</div>
<div id="footer">
<ul>
<li><a href="https://pomerium.com/">Home</a></li>
<li><a href="https://pomerium.com/docs">Docs</a></li>
<li><a href="https://pomerium.com/docs/community/">Support</a></li>
<li><a href="https://github.com/pomerium">Github</a></li>
<li class="last">
<a href="https://twitter.com/pomerium_io">@pomerium_io</a>
</li>
</ul>
<p>© Pomerium, Inc.</p>
</div>
</div>
</div>
</body>
<script>
function onDeleteDeviceCredential(evt) {
if (!confirm("Are you sure you want to delete this device credential? If a policy requires an approved device you may need to request a new approval from your administrator.")) {
evt.preventDefault();
}
}
Array.from(document.getElementsByClassName("delete-credential-form")).forEach(function(el) {
el.addEventListener("submit", onDeleteDeviceCredential);
});
</script>
</html>
{{end}}

View file

@ -1,68 +0,0 @@
{{define "webauthn.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
{{template "header.html"}}
<title>WebAuthn</title>
<script>
window.PomeriumData = {{.Data}};
</script>
</head>
<body>
<div class="inner">
<div class="header clearfix">
<div class="heading"></div>
</div>
<div class="content">
<div class="white box">
<div class="largestatus">
<div class="title-wrapper">
<span class="title">WebAuthn Registration
<span class="experimental-icon"> </span></span>
</div>
</div>
</div>
<div class="category white box webauthn">
<div class="messages">
<div class="box-inner">
<form action="{{.SelfURL}}" method="post">
{{.csrfField}}
<input type="hidden" name="action" value="register" />
<input
type="hidden"
id="register_response"
name="register_response"
/>
<input
class="button"
type="submit"
id="register_button"
value="Register New Device"
/>
</form>
<form action="{{.SelfURL}}" method="post">
{{.csrfField}}
<input type="hidden" name="action" value="authenticate" />
<input
type="hidden"
id="authenticate_response"
name="authenticate_response"
/>
<input
class="button"
type="submit"
id="authenticate_button"
value="Authenticate Existing Device"
/>
</form>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="/.pomerium/assets/js/webauthn.mjs"></script>
</body>
</html>
{{end}}

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#6e43e8" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 380 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#333333" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>

Before

Width:  |  Height:  |  Size: 249 B

View file

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<g transform="matrix(0.8333333333333334,0,0,0.8333333333333334,0,0)"><path d="M20.5,19.34,16.14,12a1,1,0,0,1-.14-.51V2.5a.5.5,0,0,1,.5-.5H18a1,1,0,0,0,0-2H6A1,1,0,0,0,6,2H7.5a.5.5,0,0,1,.5.5v9a1,1,0,0,1-.14.51L3.54,19.28A3,3,0,0,0,6,24H18a3,3,0,0,0,2.49-4.66ZM8.67,16a.5.5,0,0,1-.43-.25.5.5,0,0,1,0-.5l1.62-2.74A1,1,0,0,0,10,12V2.5a.5.5,0,0,1,.5-.5h3a.5.5,0,0,1,.5.5V5.25a.5.5,0,0,1-.5.5H12.18a.75.75,0,0,0,0,1.5H13.5a.5.5,0,0,1,.5.5v1a.5.5,0,0,1-.5.5H12.18a.75.75,0,0,0,0,1.5H13.5a.5.5,0,0,1,.5.5V12a1,1,0,0,0,.14.51l1.61,2.74a.47.47,0,0,1,0,.5.52.52,0,0,1-.44.25Zm.82,5.82a1.5,1.5,0,1,1,1.5-1.5A1.5,1.5,0,0,1,9.49,21.82Zm4.22-3a1,1,0,0,1,0-2,1,1,0,0,1,0,2Zm2.49,3.09a1,1,0,1,1,1-1A1,1,0,0,1,16.2,21.89Z" style="fill: #5E6A82"></path></g></svg>

Before

Width:  |  Height:  |  Size: 813 B

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15pt" height="15pt" viewBox="0 0 15 15" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 8.585938 4.039062 L 8.570312 0.0429688 L 6.339844 0.0429688 L 6.355469 4.039062 L 7.46875 5.570312 Z M 8.585938 4.039062 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 6.355469 10.886719 L 6.355469 14.894531 L 8.585938 14.894531 L 8.585938 10.886719 L 7.46875 9.355469 Z M 6.355469 10.886719 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,94.901961%,90.196078%);fill-opacity:1;" d="M 8.585938 10.886719 L 10.929688 14.125 L 12.726562 12.816406 L 10.382812 9.578125 L 8.585938 9 Z M 8.585938 10.886719 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,94.901961%,90.196078%);fill-opacity:1;" d="M 6.355469 4.039062 L 3.996094 0.800781 L 2.199219 2.109375 L 4.542969 5.347656 L 6.355469 5.925781 Z M 6.355469 4.039062 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,72.54902%,94.509804%);fill-opacity:1;" d="M 4.542969 5.347656 L 0.742188 4.113281 L 0.0585938 6.222656 L 3.863281 7.46875 L 5.660156 6.875 Z M 4.542969 5.347656 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,72.54902%,94.509804%);fill-opacity:1;" d="M 9.265625 8.050781 L 10.382812 9.578125 L 14.183594 10.8125 L 14.867188 8.703125 L 11.0625 7.46875 Z M 9.265625 8.050781 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(83.921569%,22.745098%,100%);fill-opacity:1;" d="M 11.0625 7.46875 L 14.867188 6.222656 L 14.183594 4.113281 L 10.382812 5.347656 L 9.265625 6.875 Z M 11.0625 7.46875 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(83.921569%,22.745098%,100%);fill-opacity:1;" d="M 3.863281 7.46875 L 0.0585938 8.703125 L 0.742188 10.8125 L 4.542969 9.578125 L 5.660156 8.050781 Z M 3.863281 7.46875 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(98.431373%,0.392157%,35.686275%);fill-opacity:1;" d="M 4.542969 9.578125 L 2.199219 12.816406 L 3.996094 14.125 L 6.355469 10.886719 L 6.355469 9 Z M 4.542969 9.578125 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(98.431373%,0.392157%,35.686275%);fill-opacity:1;" d="M 10.382812 5.347656 L 12.726562 2.109375 L 10.929688 0.800781 L 8.585938 4.039062 L 8.585938 5.925781 Z M 10.382812 5.347656 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1 +0,0 @@
<svg viewBox="0 0 1119.78 126.78" xmlns="http://www.w3.org/2000/svg"><g fill="#6e43e8" transform="translate(-76.61 -191.46)"><path d="m453.81 236.69a29.54 29.54 0 0 1 -2.81 12.83q-5.16 11.11-18.16 15.5a24.7 24.7 0 0 1 -7.82 1.57 36.4 36.4 0 0 1 -16.75-3.6v-1.57a30.48 30.48 0 0 0 21.13-2.5q8.61-4.38 11.74-14.41a20.53 20.53 0 0 0 .94-5.94v-3.45q-.31-11-5.79-17.45t-15.34-6.5h-18v93.45a10.05 10.05 0 0 0 3.05 7 9.49 9.49 0 0 0 7 2.89h2v1.57h-34.29v-1.57h2a9.49 9.49 0 0 0 7-2.89 10.05 10.05 0 0 0 3.05-7v-87.5a10.16 10.16 0 0 0 -3.28-6.49 9.61 9.61 0 0 0 -6.74-2.59h-1.87l-.16-1.56h40.85q15 0 23.64 8.06t8.61 22.15z"/><path d="m493.73 226.12a60 60 0 0 1 53.06 0 48.42 48.42 0 0 1 19.21 17.46 49.2 49.2 0 0 1 0 51 48.34 48.34 0 0 1 -19.17 17.46 53.62 53.62 0 0 1 -24.73 6.18 64.2 64.2 0 0 1 -22.39-3.6 51.55 51.55 0 0 1 -16.82-10 46.19 46.19 0 0 1 -11.31-15.62q-4.15-9.24-4.15-21.68a43.65 43.65 0 0 1 7.12-23.71 48.38 48.38 0 0 1 19.18-17.49zm-2.9 75q11.82 12.45 29.35 12.44a39.06 39.06 0 0 0 29.43-12.44q11.89-12.45 11.89-32t-11.89-32a39.06 39.06 0 0 0 -29.43-12.44q-17.53 0-29.35 12.44t-11.82 32q-.01 19.54 11.82 31.98z"/><path d="m713.18 314.48h2.35v1.57h-32.53v-1.41h2.35a5.71 5.71 0 0 0 4.15-1.8 4.94 4.94 0 0 0 1.48-4.3l-8.17-64.54-29.58 58.23q-4.38 9.56-5.48 16h-1.56l-37.73-72.79-7 63.09a4.92 4.92 0 0 0 1.48 4.22 5.78 5.78 0 0 0 4.31 1.88h2.19v1.41h-29v-1.57h2.35a10.42 10.42 0 0 0 8.77-4.07 9.51 9.51 0 0 0 1.87-4.85l12.21-85.62h1.57l41.32 79.52 39.92-79.52h1.41l12.68 85.93a9.58 9.58 0 0 0 3.6 6.27 11 11 0 0 0 7.04 2.35z"/><path d="m727.42 314.48a8.89 8.89 0 0 0 6.34-2.42 8.31 8.31 0 0 0 2.74-5.87v-74.35a7.45 7.45 0 0 0 -2.66-5.8 9.39 9.39 0 0 0 -6.42-2.34h-2.19v-1.57h45.71a59.57 59.57 0 0 0 11.27-.94 37 37 0 0 0 6.26-1.56v17.84l-1.72-.16v-2.31a8 8 0 0 0 -2.19-5.64 7.42 7.42 0 0 0 -5.64-2.35h-32.56v39.76h26.77a6 6 0 0 0 4.77-1.88 6.84 6.84 0 0 0 1.65-4.69v-2h1.72v21.92h-1.72v-2q0-4.38-3.29-5.95a11 11 0 0 0 -2.66-.62h-27.24v39.65h24.58a26.59 26.59 0 0 0 12.13-2.51 21 21 0 0 0 7.51-5.95 53.55 53.55 0 0 0 5.25-9.07h1.56l-8.61 22.38h-63.55v-1.57z"/><path d="m838.4 227h-8.29v79a8.37 8.37 0 0 0 4.38 7.36 9.4 9.4 0 0 0 4.54 1.09h2l.16 1.57h-32.06v-1.57h2.19a8.69 8.69 0 0 0 6.18-2.42 7.75 7.75 0 0 0 2.59-5.87v-74.32a7.52 7.52 0 0 0 -2.59-5.8 8.88 8.88 0 0 0 -6.18-2.34h-2.19v-1.57h39.29q14.72 0 23.24 6.73a21.46 21.46 0 0 1 8.54 17.69q0 10.95-6.89 18.31a27 27 0 0 1 -17.53 8.46q5 2.19 12.05 12.21-.17 0 5.4 6.88t8.92 10.73c2.24 2.55 4.1 4.51 5.56 5.86a18.76 18.76 0 0 0 13.3 5.48v1.57h-4.85q-12.36 0-19.41-4.85a45.75 45.75 0 0 1 -9.7-8.93q-.94-1.25-7.91-11t-7.43-10.41q-8.46-10.49-15.81-10.49v-1.56q18.32 0 22.07-1.88c3.44-1.78 5.79-3.65 7-5.64a25.44 25.44 0 0 0 3.91-13.77q0-9.56-5.63-14.72t-15.18-5.63c-2.78-.17-6.02-.17-9.67-.17z"/><path d="m909.47 314.48a11.42 11.42 0 0 0 7.35-2.5 7.42 7.42 0 0 0 3.13-6v-73.83a7.42 7.42 0 0 0 -3.13-6 11.65 11.65 0 0 0 -7.51-2.5h-2v-1.57h35.37v1.57h-2a11.6 11.6 0 0 0 -7.28 2.34 8.05 8.05 0 0 0 -3.37 5.8v74.35a7.72 7.72 0 0 0 3.29 5.87 11.65 11.65 0 0 0 7.36 2.42h2l.16 1.57h-35.56v-1.57z"/><path d="m1055.35 223.7h-2.35a7.22 7.22 0 0 0 -6.41 3.28 6.78 6.78 0 0 0 -1.1 3.29v52.73q0 16.29-10.41 25.75t-28.57 9.47q-18.15 0-28.64-9.39t-10.49-25.67v-52.73a6.18 6.18 0 0 0 -2.19-4.78 7.71 7.71 0 0 0 -5.32-2h-2.35v-1.57h29.74v1.57h-2.35a7.73 7.73 0 0 0 -5.32 2 6.18 6.18 0 0 0 -2.19 4.78v50.87q0 15.18 7.75 23.95t21.91 8.76q14.18 0 22.7-8.29t8.53-22.7v-52.59a6.71 6.71 0 0 0 -2.27-4.86 7.61 7.61 0 0 0 -5.24-1.87h-2.35v-1.57h26.92z"/><path d="m1194 314.48h2.35v1.57h-32.56v-1.41h2.35a5.69 5.69 0 0 0 4.14-1.8 4.92 4.92 0 0 0 1.49-4.3l-8.14-64.5-29.58 58.23q-4.38 9.56-5.48 16h-1.57l-37.72-72.79-7 63.09a4.92 4.92 0 0 0 1.48 4.22 5.78 5.78 0 0 0 4.31 1.88h2.19v1.41h-29v-1.57h2.35a10.42 10.42 0 0 0 8.77-4.07 9.51 9.51 0 0 0 1.87-4.85l12.21-85.62h1.57l41.32 79.52 39.92-79.52h1.41l12.67 85.93a9.63 9.63 0 0 0 3.61 6.27 11 11 0 0 0 7.04 2.31z"/><path d="m337.76 215.82a24.36 24.36 0 0 0 -24.36-24.36h-212.4a24.36 24.36 0 0 0 -24.36 24.36v100.37h16.8v-28a34.11 34.11 0 1 1 68.21 0v28h12.18v-28a34.11 34.11 0 1 1 68.21 0v28h12.18v-28a34.1 34.1 0 1 1 68.2 0v28h15.33zm-244.32 25.31a34.11 34.11 0 1 1 68.21 0zm80.39 0a34.11 34.11 0 1 1 68.21 0zm80.39 0a34.11 34.11 0 1 1 68.21 0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.15 124.73"><title>logo-only</title><path d="M742.76,162.82a24.36,24.36,0,0,0-24.36-24.36H506a24.36,24.36,0,0,0-24.36,24.36V263.19h16.83v-28h0a34.11,34.11,0,1,1,68.21,0h0v28h12.18v-28h0a34.11,34.11,0,1,1,68.21,0h0v28h12.18v-28h0a34.1,34.1,0,1,1,68.2,0h0v28h15.33ZM498.44,188.13a34.11,34.11,0,1,1,68.21,0Zm80.39,0a34.11,34.11,0,1,1,68.21,0Zm80.39,0a34.11,34.11,0,1,1,68.21,0Z" transform="translate(-481.61 -138.46)"/></svg>

Before

Width:  |  Height:  |  Size: 512 B

View file

@ -1 +0,0 @@
<svg viewBox="0 0 139 30" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-5 -5.5)"><path d="m10.6 5.5h127.8c3.09 0 5.6 2 5.6 4.39v21.22c0 2.42-2.51 4.39-5.6 4.39h-127.8c-3.09 0-5.6-2-5.6-4.39v-21.22c0-2.42 2.51-4.39 5.6-4.39z" fill="#6e43e8" fill-rule="evenodd"/><g fill="#fff"><path d="m75.4 26.62h-1.46l1.13-2.79-2.25-5.69h1.54l1.42 3.86 1.43-3.87h1.54zm-5.61-2.44a2.42 2.42 0 0 1 -1.5-.55v.37h-1.51v-8.44h1.51v3a2.48 2.48 0 0 1 1.5-.55c1.58 0 2.66 1.28 2.66 3.09s-1.08 3.08-2.66 3.08zm-.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.8s-.59-1.79-1.44-1.79zm-8.8 4.33a2.38 2.38 0 0 1 -1.5.55c-1.57 0-2.66-1.27-2.66-3.09s1.09-3.09 2.66-3.09a2.44 2.44 0 0 1 1.5.55v-3h1.52v8.45h-1.52zm0-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 1.8 1.49 3.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.13 2.87 2.87 0 0 1 2.88-3.03 2.66 2.66 0 0 1 2.59 3 5.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 -1.19-1.36zm-5.34.93v3.9h-1.5v-5.9h1.51v.59a2 2 0 0 1 1.45-.69 1.65 1.65 0 0 1 .49.06v1.35a1.83 1.83 0 0 0 -.53-.07 1.87 1.87 0 0 0 -1.41.76zm-6.7 1.41a1.69 1.69 0 0 0 1.76 1.49 3.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.13 2.87 2.87 0 0 1 2.88-3.03 2.66 2.66 0 0 1 2.6 3 5.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 -1.23-1.36zm-6.67 4.83-1.2-4-1.2 4h-1.3l-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 0 2.94 2.94 0 0 1 -3.07 3.13zm0-4.92c-.88 0-1.49.75-1.49 1.83s.61 1.83 1.49 1.83 1.53-.7 1.53-1.79-.65-1.83-1.53-1.83zm-6.62 1.87h-1.36v2.91h-1.49v-8.07h2.87a2.61 2.61 0 1 1 0 5.2zm-.22-4h-1.14v2.81h1.14a1.38 1.38 0 1 0 0-2.75z" fill-rule="evenodd"/><path d="m132.71 14.9a3.93 3.93 0 0 0 -3.93-3.9h-34.19a3.93 3.93 0 0 0 -3.93 3.92v16.14h2.71v-4.51a5.49 5.49 0 1 1 11 0v4.51h2v-4.51a5.49 5.49 0 1 1 11 0v4.51h2v-4.51a5.49 5.49 0 1 1 11 0v4.51h2.47zm-39.34 4.1a5.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"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M11.99 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm3.61 6.34c1.07 0 1.93.86 1.93 1.93 0 1.07-.86 1.93-1.93 1.93-1.07 0-1.93-.86-1.93-1.93-.01-1.07.86-1.93 1.93-1.93zm-6-1.58c1.3 0 2.36 1.06 2.36 2.36 0 1.3-1.06 2.36-2.36 2.36s-2.36-1.06-2.36-2.36c0-1.31 1.05-2.36 2.36-2.36zm0 9.13v3.75c-2.4-.75-4.3-2.6-5.14-4.96 1.05-1.12 3.67-1.69 5.14-1.69.53 0 1.2.08 1.9.22-1.64.87-1.9 2.02-1.9 2.68zM11.99 20c-.27 0-.53-.01-.79-.04v-4.07c0-1.42 2.94-2.13 4.4-2.13 1.07 0 2.92.39 3.84 1.15-1.17 2.97-4.06 5.09-7.45 5.09z"/><path fill="none" d="M0 0h24v24H0z"/></svg>

Before

Width:  |  Height:  |  Size: 670 B

View file

@ -1,119 +0,0 @@
import {
encodeUrl as base64encode,
decode as base64decode,
} from "./base64.mjs";
function decode(raw) {
return base64decode(raw);
}
function encode(raw) {
return base64encode(raw);
}
async function authenticate(requestOptions) {
const credential = await navigator.credentials.get({
publicKey: {
allowCredentials: requestOptions.allowCredentials.map((c) => ({
type: c.type,
id: decode(c.id),
})),
challenge: decode(requestOptions.challenge),
timeout: requestOptions.timeout,
userVerification: requestOptions.userVerification,
},
});
const inputEl = document.getElementById("authenticate_response");
inputEl.value = JSON.stringify({
id: credential.id,
type: credential.type,
rawId: encode(credential.rawId),
response: {
authenticatorData: encode(credential.response.authenticatorData),
clientDataJSON: encode(credential.response.clientDataJSON),
signature: encode(credential.response.signature),
userHandle: encode(credential.response.userHandle),
},
});
inputEl.parentElement.submit();
}
async function register(creationOptions) {
const credential = await navigator.credentials.create({
publicKey: {
attestation: creationOptions.attestation || undefined,
authenticatorSelection: {
authenticatorAttachment:
creationOptions.authenticatorSelection.authenticatorAttachment ||
undefined,
requireResidentKey:
creationOptions.authenticatorSelection.requireResidentKey ||
undefined,
residentKey: creationOptions.authenticatorSelection.residentKey,
userVerification:
creationOptions.authenticatorSelection.userVerification || undefined,
},
challenge: decode(creationOptions.challenge),
pubKeyCredParams: creationOptions.pubKeyCredParams.map((p) => ({
type: p.type,
alg: p.alg,
})),
rp: {
name: creationOptions.rp.name,
},
timeout: creationOptions.timeout,
user: {
id: decode(creationOptions.user.id),
name: creationOptions.user.name,
displayName: creationOptions.user.displayName,
},
},
});
const inputEl = document.getElementById("register_response");
inputEl.value = JSON.stringify({
id: credential.id,
type: credential.type,
rawId: encode(credential.rawId),
response: {
attestationObject: encode(credential.response.attestationObject),
clientDataJSON: encode(credential.response.clientDataJSON),
},
});
inputEl.parentElement.submit();
}
function init() {
if (!("PomeriumData" in window)) {
return;
}
const requestOptions = window.PomeriumData.requestOptions;
const authenticateButton = document.getElementById("authenticate_button");
if (authenticateButton) {
if (
requestOptions.allowCredentials &&
requestOptions.allowCredentials.length > 0
) {
authenticateButton.addEventListener("click", function(evt) {
evt.preventDefault();
authenticate(requestOptions);
});
} else {
authenticateButton.addEventListener("click", function(evt) {
evt.preventDefault();
});
authenticateButton.setAttribute("disabled", "DISABLED");
}
}
const creationOptions = window.PomeriumData.creationOptions;
const registerButton = document.getElementById("register_button");
if (registerButton) {
registerButton.addEventListener("click", function(evt) {
evt.preventDefault();
register(creationOptions);
});
}
}
init();

View file

@ -1,507 +0,0 @@
/******* Global *******/
body,
div,
dl,
dt,
dd,
ul,
ol,
li,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
code,
form,
fieldset,
legend,
input,
button,
textarea,
p,
blockquote,
th,
td {
margin: 0;
padding: 0;
}
fieldset,
img {
border: 0;
}
address,
caption,
cite,
code,
dfn,
em,
strong,
th,
var,
optgroup {
font-style: inherit;
font-weight: inherit;
}
del,
ins {
text-decoration: none;
}
li {
list-style: none;
}
caption,
th {
text-align: left;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
font-weight: normal;
}
q:before,
q:after {
content: "";
}
abbr,
acronym {
border: 0;
font-variant: normal;
}
sup {
vertical-align: baseline;
}
sub {
vertical-align: baseline;
}
legend {
color: #000;
}
input,
button,
textarea,
select,
optgroup,
option {
font-family: inherit;
font-size: inherit;
font-style: inherit;
font-weight: inherit;
}
input,
button,
textarea,
select {
*font-size: 100%;
}
aside,
dialog,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
html,
body {
margin: 0;
padding: 0;
}
html {
background: #f6f9fc;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.inner {
width: 614px;
margin: auto;
margin-bottom: 2em;
}
.box {
overflow: hidden;
border-radius: 4px;
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);
}
.box-inner {
padding: 35px;
}
.box-inner ~ .box-inner {
padding-top: 0;
}
.white {
background: white;
}
h2 {
font-size: 1.5em;
font-style: normal;
color: #444;
margin: 0 0 0.8em 0;
padding-bottom: 0.2em;
border-bottom: 1px solid #eee;
}
ul.plain {
list-style: none;
-webkit-padding-start: 0;
padding-left: 0;
}
a {
color: #6e43e8;
text-decoration: none;
}
a:hover {
color: #32325d;
}
/******* Status/Graph Colors *******/
.status-up .status-time {
color: #3ecf8e;
}
.status-up {
background: #3ecf8e;
}
.status-down .status-time {
color: #ffe7cb;
}
.status-down {
background: #e25950;
}
/******* Clearfix *******/
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
* html .clearfix {
zoom: 1;
} /* IE6 */
*:first-child + html .clearfix {
zoom: 1;
} /* IE7 */
/******* Header *******/
.header {
padding: 30px 0;
height: 40px;
position: relative;
}
.header span {
color: #6e43e8;
font-size: 16px;
line-height: 31px;
position: absolute;
left: 540px;
}
.heading {
float: left;
margin: 7px 1px;
}
.statuses {
float: left;
}
.logo {
display: inline-block;
position: relative;
background: url(/.pomerium/assets/img/logo-long.svg) no-repeat;
width: 663px;
height: 26px;
cursor: pointer;
}
.logo:hover {
opacity: 0.7;
}
/******* Content *******/
.largestatus {
border: 0;
position: relative;
z-index: 10;
padding: 0 36px;
padding-left: 84px;
min-height: 155px;
}
.largestatus .title {
display: block;
padding-top: 46px;
margin-bottom: 10px;
font-size: 29px;
color: #32325d;
}
.largestatus .status-time {
display: block;
font-size: 14px;
color: #8898aa;
padding-bottom: 46px;
}
.category {
margin-top: 40px;
}
/******* Statuses *******/
.statuses {
font-size: 0.7em;
}
.status-bubble {
width: 44px;
height: 44px;
position: absolute;
left: 24px;
top: 52px;
background-position: center;
background-repeat: no-repeat;
border-radius: 50%;
}
.title-wrapper {
display: inline-block;
color: #333;
min-height: 155px;
}
.status-time {
color: #999;
}
/******* category *******/
.category {
background: white;
}
div.category-header {
margin-bottom: 25px;
}
.category-title {
color: #525f7f;
font-size: 15px;
padding-right: 10px;
}
.category-icon {
display: block;
position: relative;
top: 2px;
float: right;
width: 27px;
height: 25px;
background: url(/.pomerium/assets/img/jwt.svg) 100% 0 no-repeat;
}
.experimental-icon {
display: block;
position: relative;
top: 2px;
float: right;
width: 27px;
height: 25px;
background: url(/.pomerium/assets/img/experimental.svg) 100% 0 no-repeat;
}
div.category-link {
background: #f6f9fc;
padding: 25px 36px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
font-size: 13px;
color: #6b7c93;
}
/* Footer */
div#footer {
/*background: rgba(0,0,0,0.05);*/
margin: 0 0 0;
padding: 40px 0 0px;
position: relative;
font-size: 13px;
}
div#footer ul {
padding-left: 10px;
}
div#footer li {
display: inline;
padding-right: 15px;
}
div#footer a {
color: #8898aa;
text-decoration: none;
}
div#footer a:hover {
color: #32325d;
}
div#footer li.last {
border: 0;
}
div#footer p {
color: #8898aa;
position: absolute;
right: 10px;
top: 40px;
}
/* - Tables */
table tbody tr td {
border-color: #525f7f;
border-style: solid;
border: 0;
padding: 16px 16px;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: auto;
}
table:not(:first-child) {
margin-top: 20px;
}
table table {
margin: 16px 0 8px 0;
}
table thead tr th {
font-weight: 500;
font-size: 13px;
color: #6e43e8;
text-transform: uppercase;
text-align: left;
padding: 0 0 8px 16px;
}
table thead tr th p {
font-size: 13px;
text-transform: uppercase;
padding: 0;
line-height: 15px;
}
table tbody,
table tbody td > * {
font-size: 14px;
line-height: 20px;
vertical-align: top;
padding-top: 0;
overflow-wrap: anywhere;
}
table tbody tr td {
border-color: rgb(227, 232, 238);
border-style: solid;
padding: 16px 16px;
min-width: 80px;
}
table tbody tr td:first-child {
border-left-width: 1px;
}
table tbody tr td:last-child {
border-right-width: 1px;
}
table tbody tr:first-child > td {
border-top-width: 1px;
}
table tbody tr td {
border-bottom-width: 1px;
}
table tbody tr:first-child > td:first-child {
border-top-left-radius: 1px;
}
table tbody tr:first-child > td:last-child {
border-top-right-radius: 1px;
}
table tbody tr:last-child > td:first-child {
border-bottom-left-radius: 1px;
}
table tbody tr:last-child > td:last-child {
border-bottom-right-radius: 1px;
}
table tbody tr:nth-child(2n + 1) td {
background: #f6f9fc;
}
input,
button,
a.button {
background: #6e43e8;
box-shadow: 0 2px 5px 0 rgba(50, 50, 93, 0.1), 0 1px 1px 0 rgba(0, 0, 0, 0.07);
border-radius: 4px;
height: 32px;
font-size: 16px;
color: #f6f9fc;
font-weight: 500;
padding: 0 12px;
cursor: pointer;
outline: none;
display: inline-block;
text-decoration: none;
text-transform: none;
white-space: nowrap;
transition: box-shadow 150ms ease-in-out;
}
button:disabled, input:disabled {
cursor: default;
background:#cccccc;
color: #838383;
}
.webauthn .box-inner {
text-align: center;
}
.webauthn form {
display: inline-block;
}

View file

@ -1,119 +0,0 @@
// Package frontend handles the generation, and instantiation of Pomerium's
// html templates.
package frontend
import (
"embed"
"encoding/base64"
"fmt"
"html/template"
"io/fs"
"mime"
"net/http"
"os"
"path"
"strings"
"time"
)
// FS is the frontend assets file system.
//go:embed assets
var FS embed.FS
// NewTemplates loads pomerium's templates. Panics on failure.
func NewTemplates() (*template.Template, error) {
assetsFS, err := fs.Sub(FS, "assets")
if err != nil {
return nil, err
}
dataURLs := map[string]template.URL{}
err = fs.WalkDir(assetsFS, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
bs, err := fs.ReadFile(assetsFS, p)
if err != nil {
return fmt.Errorf("internal/frontend: error reading %s: %w", p, err)
}
encoded := base64.StdEncoding.EncodeToString(bs)
dataURLs[p] = template.URL(fmt.Sprintf(
"data:%s;base64,%s", mime.TypeByExtension(path.Ext(p)), encoded))
return nil
})
if err != nil {
return nil, err
}
t := template.New("pomerium-templates").Funcs(map[string]interface{}{
"safeURL": func(arg interface{}) template.URL {
return template.URL(fmt.Sprint(arg))
},
"safeHTML": func(arg interface{}) template.HTML {
return template.HTML(fmt.Sprint(arg))
},
"safeHTMLAttr": func(arg interface{}) template.HTMLAttr {
return template.HTMLAttr(fmt.Sprint(arg))
},
"dataURL": func(p string) template.URL {
return dataURLs[strings.TrimPrefix(p, "/.pomerium/assets/")]
},
"formatTime": func(arg interface{}) string {
var tm time.Time
switch t := arg.(type) {
case float64:
tm = time.Unix(int64(t), 0)
case int:
tm = time.Unix(int64(t), 0)
case int64:
tm = time.Unix(t, 0)
case time.Time:
tm = t
default:
return "<INVALID TIME>"
}
return tm.Format("2006-01-02 15:04:05 MST")
},
})
err = fs.WalkDir(assetsFS, "html", func(p string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
bs, err := fs.ReadFile(assetsFS, p)
if err != nil {
return fmt.Errorf("internal/frontend: error reading %s: %w", p, err)
}
_, err = t.Parse(string(bs))
if err != nil {
return fmt.Errorf("internal/frontend: error parsing template %s: %w", p, err)
}
}
return nil
})
if err != nil {
return nil, err
}
return t, nil
}
// MustAssetHandler wraps a call to the embedded static file system and panics
// if the error is non-nil. It is intended for use in variable initializations
func MustAssetHandler() http.Handler {
assetsFS, err := fs.Sub(FS, "assets")
if err != nil {
panic(err)
}
return http.FileServer(http.FS(assetsFS))
}

View file

@ -1,20 +0,0 @@
package frontend
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewTemplates(t *testing.T) {
tpl, err := NewTemplates()
require.NoError(t, err)
var buf bytes.Buffer
err = tpl.ExecuteTemplate(&buf, "header.html", nil)
require.NoError(t, err)
assert.Contains(t, buf.String(), `<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />`)
}

View file

@ -1,16 +1,13 @@
package httputil package httputil
import ( import (
"html/template"
"net/http" "net/http"
"net/url" "net/url"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/telemetry/requestid" "github.com/pomerium/pomerium/internal/telemetry/requestid"
"github.com/pomerium/pomerium/ui"
) )
var errorTemplate = template.Must(frontend.NewTemplates())
// HTTPError contains an HTTP status code and wrapped error. // HTTPError contains an HTTP status code and wrapped error.
type HTTPError struct { type HTTPError struct {
// HTTP status codes as registered with IANA. // HTTP status codes as registered with IANA.
@ -68,7 +65,21 @@ func (e *HTTPError) ErrorResponse(w http.ResponseWriter, r *http.Request) {
RenderJSON(w, e.Status, response) RenderJSON(w, e.Status, response)
return return
} }
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
w.WriteHeader(e.Status) m := map[string]interface{}{
errorTemplate.ExecuteTemplate(w, "error.html", response) "canDebug": response.CanDebug,
"error": response.Error,
"requestId": response.RequestID,
"status": response.Status,
"statusText": response.StatusText,
"version": response.Version,
}
if response.DebugURL != nil {
m["debugUrl"] = response.DebugURL.String()
}
w.WriteHeader(response.Status)
if err := ui.ServePage(w, r, "Error", m); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} }

View file

@ -38,7 +38,7 @@ const (
// by default includes profile photo exceptions for supported identity providers. // by default includes profile photo exceptions for supported identity providers.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
var HeadersContentSecurityPolicy = map[string]string{ var HeadersContentSecurityPolicy = map[string]string{
"Content-Security-Policy": "default-src 'none'; style-src 'self' data:; img-src * data:; script-src 'self' 'unsafe-inline'", "Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline' data:; img-src * data:; script-src 'self' 'unsafe-inline'; font-src data:",
"Referrer-Policy": "Same-origin", "Referrer-Policy": "Same-origin",
} }

View file

@ -9,10 +9,19 @@ import (
"testing" "testing"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/testing/protocmp"
) )
// AssertProtoEqual asserts that two protobuf messages equal. Slices of messages are also supported.
func AssertProtoEqual(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) bool {
t.Helper()
return assert.True(t, cmp.Equal(expected, actual, protocmp.Transform()),
cmp.Diff(expected, actual, protocmp.Transform()))
}
// AssertProtoJSONEqual asserts that a protobuf message matches the given JSON. The protoMsg can also be a slice // AssertProtoJSONEqual asserts that a protobuf message matches the given JSON. The protoMsg can also be a slice
// of protobuf messages. // of protobuf messages.
func AssertProtoJSONEqual(t *testing.T, expected string, protoMsg interface{}, msgAndArgs ...interface{}) bool { func AssertProtoJSONEqual(t *testing.T, expected string, protoMsg interface{}, msgAndArgs ...interface{}) bool {

62
internal/urlutil/known.go Normal file
View file

@ -0,0 +1,62 @@
package urlutil
import (
"net/http"
"net/url"
)
// DefaultDeviceType is the default device type when none is specified.
const DefaultDeviceType = "any"
// RedirectURL returns the redirect URL from the query string or a cookie.
func RedirectURL(r *http.Request) (string, bool) {
if v := r.FormValue(QueryRedirectURI); v != "" {
return v, true
}
if c, err := r.Cookie(QueryRedirectURI); err == nil {
return c.Value, true
}
return "", false
}
// SignOutURL returns the /.pomerium/sign_out URL.
func SignOutURL(r *http.Request, authenticateURL *url.URL, key []byte) string {
u := authenticateURL.ResolveReference(&url.URL{
Path: "/.pomerium/sign_out",
})
if redirectURI, ok := RedirectURL(r); ok {
u.RawQuery = (&url.Values{
QueryRedirectURI: {redirectURI},
}).Encode()
}
return NewSignedURL(key, u).Sign().String()
}
// WebAuthnURL returns the /.pomerium/webauthn URL.
func WebAuthnURL(r *http.Request, authenticateURL *url.URL, key []byte, values url.Values) string {
u := authenticateURL.ResolveReference(&url.URL{
Path: "/.pomerium/webauthn",
RawQuery: buildURLValues(values, url.Values{
QueryDeviceType: {DefaultDeviceType},
QueryEnrollmentToken: nil,
QueryRedirectURI: {authenticateURL.ResolveReference(&url.URL{
Path: "/.pomerium/device-enrolled",
}).String()},
}).Encode(),
})
return NewSignedURL(key, u).Sign().String()
}
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
}

View file

@ -1,4 +1,4 @@
package authenticate package urlutil
import ( import (
"net/http" "net/http"
@ -8,31 +8,27 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/internal/urlutil"
) )
func TestAuthenticate_getRedirectURI(t *testing.T) { func TestRedirectURI(t *testing.T) {
t.Run("query", func(t *testing.T) { t.Run("query", func(t *testing.T) {
r, err := http.NewRequest("GET", "https://www.example.com?"+(url.Values{ r, err := http.NewRequest("GET", "https://www.example.com?"+(url.Values{
urlutil.QueryRedirectURI: {"https://www.example.com/redirect"}, QueryRedirectURI: {"https://www.example.com/redirect"},
}).Encode(), nil) }).Encode(), nil)
require.NoError(t, err) require.NoError(t, err)
a := new(Authenticate) redirectURI, ok := RedirectURL(r)
redirectURI, ok := a.getRedirectURI(r)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, "https://www.example.com/redirect", redirectURI) assert.Equal(t, "https://www.example.com/redirect", redirectURI)
}) })
t.Run("form", func(t *testing.T) { t.Run("form", func(t *testing.T) {
r, err := http.NewRequest("POST", "https://www.example.com", strings.NewReader((url.Values{ r, err := http.NewRequest("POST", "https://www.example.com", strings.NewReader((url.Values{
urlutil.QueryRedirectURI: {"https://www.example.com/redirect"}, QueryRedirectURI: {"https://www.example.com/redirect"},
}).Encode())) }).Encode()))
require.NoError(t, err) require.NoError(t, err)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
a := new(Authenticate) redirectURI, ok := RedirectURL(r)
redirectURI, ok := a.getRedirectURI(r)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, "https://www.example.com/redirect", redirectURI) assert.Equal(t, "https://www.example.com/redirect", redirectURI)
}) })
@ -40,12 +36,11 @@ func TestAuthenticate_getRedirectURI(t *testing.T) {
r, err := http.NewRequest("GET", "https://www.example.com", nil) r, err := http.NewRequest("GET", "https://www.example.com", nil)
require.NoError(t, err) require.NoError(t, err)
r.AddCookie(&http.Cookie{ r.AddCookie(&http.Cookie{
Name: urlutil.QueryRedirectURI, Name: QueryRedirectURI,
Value: "https://www.example.com/redirect", Value: "https://www.example.com/redirect",
}) })
a := new(Authenticate) redirectURI, ok := RedirectURL(r)
redirectURI, ok := a.getRedirectURI(r)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, "https://www.example.com/redirect", redirectURI) assert.Equal(t, "https://www.example.com/redirect", redirectURI)
}) })

View file

@ -8,12 +8,13 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"github.com/pomerium/pomerium/internal/urlutil"
"github.com/pomerium/pomerium/pkg/grpc/databroker" "github.com/pomerium/pomerium/pkg/grpc/databroker"
"github.com/pomerium/pomerium/pkg/grpc/device" "github.com/pomerium/pomerium/pkg/grpc/device"
) )
// DefaultDeviceType is the default device type when none is specified. // DefaultDeviceType is the default device type when none is specified.
const DefaultDeviceType = "any" const DefaultDeviceType = urlutil.DefaultDeviceType
var supportedPublicKeyCredentialParameters = []*device.WebAuthnOptions_PublicKeyCredentialParameters{ var supportedPublicKeyCredentialParameters = []*device.WebAuthnOptions_PublicKeyCredentialParameters{
{Type: device.WebAuthnOptions_PUBLIC_KEY, Alg: int64(cose.AlgorithmES256)}, {Type: device.WebAuthnOptions_PUBLIC_KEY, Alg: int64(cose.AlgorithmES256)},

View file

@ -7,14 +7,12 @@ package proxy
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"sync/atomic" "sync/atomic"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/frontend"
"github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry/metrics" "github.com/pomerium/pomerium/internal/telemetry/metrics"
@ -49,7 +47,6 @@ func ValidateOptions(o *config.Options) error {
// Proxy stores all the information associated with proxying a request. // Proxy stores all the information associated with proxying a request.
type Proxy struct { type Proxy struct {
templates *template.Template
state *atomicProxyState state *atomicProxyState
currentOptions *config.AtomicOptions currentOptions *config.AtomicOptions
currentRouter atomic.Value currentRouter atomic.Value
@ -64,7 +61,6 @@ func New(cfg *config.Config) (*Proxy, error) {
} }
p := &Proxy{ p := &Proxy{
templates: template.Must(frontend.NewTemplates()),
state: newAtomicProxyState(state), state: newAtomicProxyState(state),
currentOptions: config.NewAtomicOptions(), currentOptions: config.NewAtomicOptions(),
} }

View file

@ -5,7 +5,7 @@ _dir=/tmp/pomerium-dev-docker
mkdir -p "$_dir" mkdir -p "$_dir"
# build linux binary # build linux binary
env GOOS=linux make build-deps build env GOOS=linux make build
cp bin/pomerium $_dir/ cp bin/pomerium $_dir/
# build docker image # build docker image

BIN
ui/dist/apple-touch-icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
ui/dist/favicon-16x16.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

BIN
ui/dist/favicon-32x32.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

BIN
ui/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

28
ui/dist/index.html vendored Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="/.pomerium/favicon.ico?v=2" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/.pomerium/apple-touch-icon.png"
/>
<link rel="icon" sizes="32x32" href="/.pomerium/favicon-32x32.png" />
<link rel="icon" sizes="16x16" href="/.pomerium/favicon-16x16.png" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>User info dashboard</title>
<link rel="stylesheet" href="/.pomerium/index.css" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
window.POMERIUM_DATA = {};
</script>
<script src="/.pomerium/index.js"></script>
</body>
</html>

93
ui/embed.go Normal file
View file

@ -0,0 +1,93 @@
// Package ui contains the user info dashboard ui.
package ui
import (
"bytes"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"time"
"github.com/pomerium/csrf"
)
var (
//go:embed dist/*
uiFS embed.FS
)
// ServeFile serves a file.
func ServeFile(w http.ResponseWriter, r *http.Request, filePath string) error {
f, etag, err := openLocalOrEmbeddedFile(filepath.Join("dist", filePath))
if err != nil {
return err
}
defer f.Close()
w.Header().Set("ETag", `"`+etag+`"`)
http.ServeContent(w, r, filepath.Base(filePath), time.Time{}, f.(io.ReadSeeker))
return nil
}
// ServePage serves the index.html page.
func ServePage(w http.ResponseWriter, r *http.Request, page string, data map[string]interface{}) error {
if data == nil {
data = make(map[string]interface{})
}
data["csrfToken"] = csrf.Token(r)
data["page"] = page
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
f, _, err := openLocalOrEmbeddedFile("dist/index.html")
if err != nil {
return err
}
bs, err := io.ReadAll(f)
_ = f.Close()
if err != nil {
return err
}
bs = bytes.Replace(bs,
[]byte("window.POMERIUM_DATA = {}"),
append([]byte("window.POMERIUM_DATA = "), jsonData...),
1)
http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(bs))
return nil
}
var startTime = time.Now()
func openLocalOrEmbeddedFile(name string) (f fs.File, etag string, err error) {
f, err = os.Open(filepath.Join("ui", name))
if os.IsNotExist(err) {
f, err = uiFS.Open(name)
}
if err != nil {
return nil, "", err
}
fi, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, "", err
}
modTime := fi.ModTime()
if modTime.IsZero() {
modTime = startTime
}
etag = fmt.Sprintf("%x", modTime.UnixNano())
return f, etag, nil
}

53
ui/package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "pomerium",
"version": "1.0.0",
"main": "src/index.tsx",
"license": "Apache-2.0",
"scripts": {
"build": "ts-node ./scripts/esbuild.ts",
"format": "prettier --write .",
"lint": "eslint .",
"watch": "ts-node ./scripts/esbuild.ts --watch"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@babel/core": "^7.0.0",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@fontsource/dm-mono": "^4.5.2",
"@fontsource/dm-sans": "^4.5.1",
"@mui/icons-material": "^5.3.1",
"@mui/material": "^5.4.0",
"luxon": "^2.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-feather": "^2.0.9"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "2.0.4",
"@types/luxon": "^2.0.9",
"@types/node": "^17.0.14",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.11",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"esbuild": "^0.13.12",
"eslint": "7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-react": "^7.28.0",
"prettier": "^2.4.1",
"ts-node": "^10.4.0",
"typescript": "^4.4.4"
}
}

16
ui/scripts/esbuild.ts Normal file
View file

@ -0,0 +1,16 @@
import { build } from "esbuild";
build({
entryPoints: ["src/index.tsx"],
bundle: true,
outfile: "dist/index.js",
sourcemap: "inline",
watch: process.argv.includes("--watch"),
minify: !process.argv.includes("--watch"),
logLevel: "info",
loader: {
".svg": "dataurl",
".woff": "dataurl",
".woff2": "dataurl",
},
});

47
ui/src/App.tsx Normal file
View file

@ -0,0 +1,47 @@
import DeviceEnrolledPage from "./components/DeviceEnrolledPage";
import ErrorPage from "./components/ErrorPage";
import Footer from "./components/Footer";
import Header from "./components/Header";
import UserInfoPage from "./components/UserInfoPage";
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
import { createTheme } from "./theme";
import { PageData } from "./types";
import Container from "@mui/material/Container";
import CssBaseline from "@mui/material/CssBaseline";
import Stack from "@mui/material/Stack";
import { ThemeProvider } from "@mui/material/styles";
import React, { FC } from "react";
const theme = createTheme();
const App: FC = () => {
const data = (window["POMERIUM_DATA"] || {}) as PageData;
let body: React.ReactNode = <></>;
switch (data?.page) {
case "DeviceEnrolled":
body = <DeviceEnrolledPage data={data} />;
break;
case "Error":
body = <ErrorPage data={data} />;
break;
case "UserInfo":
body = <UserInfoPage data={data} />;
break;
case "WebAuthnRegistration":
body = <WebAuthnRegistrationPage data={data} />;
break;
}
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container maxWidth="md" disableGutters>
<Stack spacing={3}>
<Header csrfToken={data?.csrfToken} signOutUrl={data?.signOutUrl} />
{body}
<Footer />
</Stack>
</Container>
</ThemeProvider>
);
};
export default App;

View file

@ -0,0 +1,30 @@
import Alert, { AlertColor } from "@mui/material/Alert";
import Dialog, { DialogProps } from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import React, { FC } from "react";
export type AlertDialogProps = DialogProps & {
title?: React.ReactNode;
severity?: AlertColor;
actions?: React.ReactNode;
};
export const AlertDialog: FC<AlertDialogProps> = ({
title,
severity,
children,
actions,
...props
}) => {
return (
<Dialog transitionDuration={{ exit: 0 }} {...props}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<Alert severity={severity || "info"}>{children}</Alert>
</DialogContent>
{actions ? <DialogActions>{actions}</DialogActions> : <></>}
</Dialog>
);
};
export default AlertDialog;

View file

@ -0,0 +1,24 @@
import IDField from "./IDField";
import { DateTime } from "luxon";
import React, { FC } from "react";
const unixSecondTimestampFields = new Set(["exp", "iat", "nbf", "auth_time"]);
const idFields = new Set(["groups", "jti", "oid", "tid", "wids"]);
type ClaimValueProps = {
claimKey: string;
claimValue: unknown;
};
const ClaimValue: FC<ClaimValueProps> = ({ claimKey, claimValue }) => {
if (unixSecondTimestampFields.has(claimKey)) {
return <>{DateTime.fromMillis((claimValue as number) * 1000).toISO()}</>;
}
if (idFields.has(claimKey)) {
return <IDField value={`${claimValue}`} />;
}
return <>{`${claimValue}`}</>;
};
export default ClaimValue;

View file

@ -0,0 +1,57 @@
import { Claims } from "../types";
import ClaimValue from "./ClaimValue";
import Alert from "@mui/material/Alert";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import React, { FC } from "react";
type ClaimsTableProps = {
claims: Claims;
};
const ClaimsTable: FC<ClaimsTableProps> = ({ claims }) => {
const entries = Object.entries(claims || {});
entries.sort(([a], [b]) => a.localeCompare(b));
return (
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell variant="head">Claims</TableCell>
<TableCell variant="head"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{entries.length > 0 ? (
entries.map(([key, values]) => (
<TableRow key={key}>
<TableCell>{key}</TableCell>
<TableCell>
{values?.map((v, i) => (
<React.Fragment key={`${v}`}>
{i > 0 ? <br /> : <></>}
<ClaimValue claimKey={key} claimValue={v} />
</React.Fragment>
))}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={2} padding="none">
<Alert severity="warning" square={true}>
No Claims Found
</Alert>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
);
};
export default ClaimsTable;

View file

@ -0,0 +1,9 @@
import React, { FC } from "react";
export type CsrfInputProps = {
csrfToken: string;
};
export const CsrfInput: FC<CsrfInputProps> = ({ csrfToken }) => {
return <input type="hidden" name="_pomerium_csrf" value={csrfToken} />;
};
export default CsrfInput;

View file

@ -0,0 +1,72 @@
import IDField from "./IDField";
import Alert from "@mui/material/Alert";
import Button from "@mui/material/Button";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import React, { FC } from "react";
export type DeviceCredentialsTableProps = {
csrfToken: string;
ids: string[];
webAuthnUrl: string;
};
export const DeviceCredentialsTable: FC<DeviceCredentialsTableProps> = ({
csrfToken,
ids,
webAuthnUrl
}) => {
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{ids?.length > 0 ? (
ids?.map((id) => (
<TableRow key={id}>
<TableCell>
<IDField value={id} />
</TableCell>
<TableCell>
<form action={webAuthnUrl} method="POST">
<input
type="hidden"
name="_pomerium_csrf"
value={csrfToken}
/>
<input type="hidden" name="action" value="unregister" />
<input
type="hidden"
name="pomerium_device_credential_id"
value={id}
/>
<Button size="small" type="submit" variant="contained">
Delete
</Button>
</form>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={2} padding="none">
<Alert severity="warning" square>
No device credentials found.
</Alert>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
);
};
export default DeviceCredentialsTable;

View file

@ -0,0 +1,19 @@
import HeroSection from "./HeroSection";
import Container from "@mui/material/Container";
import React, { FC } from "react";
import { DeviceEnrolledPageData } from "src/types";
type DeviceEnrolledPageProps = {
data: DeviceEnrolledPageData;
};
const DeviceEnrolledPage: FC<DeviceEnrolledPageProps> = () => {
return (
<Container>
<HeroSection
title="Device Enrolled"
text="Device Successfully Enrolled"
/>
</Container>
);
};
export default DeviceEnrolledPage;

View file

@ -0,0 +1,44 @@
import { ErrorPageData } from "../types";
import SectionFooter from "./SectionFooter";
import Alert from "@mui/material/Alert";
import AlertTitle from "@mui/material/AlertTitle";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import React, { FC } from "react";
export type ErrorPageProps = {
data: ErrorPageData;
};
export const ErrorPage: FC<ErrorPageProps> = ({ data }) => {
return (
<Container>
<Paper sx={{ overflow: "hidden" }}>
<Stack>
<Box sx={{ padding: "16px" }}>
<Alert severity="error">
<AlertTitle>
{data?.status || 500}{" "}
{data?.statusText || "Internal Server Error"}
</AlertTitle>
{data?.error || "Internal Server Error"}
</Alert>
</Box>
{data?.requestId ? (
<SectionFooter>
<Typography variant="caption">
If you should have access, contact your administrator with your
request id {data?.requestId}.
</Typography>
</SectionFooter>
) : (
<></>
)}
</Stack>
</Paper>
</Container>
);
};
export default ErrorPage;

View file

@ -0,0 +1,16 @@
import createSvgIcon from "@mui/material/utils/createSvgIcon";
import React from "react";
const ExperimentalIcon = createSvgIcon(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
d="M20.5 19.34 16.14 12a1 1 0 0 1-.14-.51V2.5a.5.5 0 0 1 .5-.5H18a1 1 0 0 0 0-2H6a1 1 0 0 0 0 2h1.5a.5.5 0 0 1 .5.5v9a1 1 0 0 1-.14.51l-4.32 7.27A3 3 0 0 0 6 24h12a3 3 0 0 0 2.49-4.66ZM8.67 16a.5.5 0 0 1-.43-.25.5.5 0 0 1 0-.5l1.62-2.74A1 1 0 0 0 10 12V2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v2.75a.5.5 0 0 1-.5.5h-1.32a.75.75 0 0 0 0 1.5h1.32a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1.32a.75.75 0 0 0 0 1.5h1.32a.5.5 0 0 1 .5.5V12a1 1 0 0 0 .14.51l1.61 2.74a.47.47 0 0 1 0 .5.52.52 0 0 1-.44.25Zm.82 5.82a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5Zm4.22-3a1 1 0 0 1 0-2 1 1 0 0 1 0 2Zm2.49 3.09a1 1 0 1 1 1-1 1 1 0 0 1-1 .98Z"
style={{
fill: "#5e6a82"
}}
transform="scale(.83333)"
/>
</svg>,
"Experimental"
);
export default ExperimentalIcon;

View file

@ -0,0 +1,39 @@
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Stack from "@mui/material/Stack";
import React, { FC } from "react";
const Footer: FC = () => {
return (
<Container component="footer">
<Stack
direction="row"
spacing={2}
sx={{
fontSize: "0.85rem",
padding: "16px"
}}
>
<Box>
<a href="https://pomerium.com/">Home</a>
</Box>
<Box>
<a href="https://pomerium.com/docs">Docs</a>
</Box>
<Box>
<a href="https://pomerium.com/docs/community/">Support</a>
</Box>
<Box>
<a href="https://github.com/pomerium">GitHub</a>
</Box>
<Box>
<a href="https://twitter.com/pomerium_io">@pomerium_io</a>
</Box>
<Box flexGrow={1} sx={{ textAlign: "right" }}>
© Pomerium, Inc.
</Box>
</Stack>
</Container>
);
};
export default Footer;

View file

@ -0,0 +1,41 @@
import { Group } from "../types";
import IDField from "./IDField";
import Section from "./Section";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import React, { FC } from "react";
export type GroupDetailsProps = {
groups: Group[];
};
export const GroupDetails: FC<GroupDetailsProps> = ({ groups }) => {
return (
<Section title="Groups">
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Name</TableCell>
</TableRow>
</TableHead>
<TableBody>
{groups?.map((group) => (
<TableRow key={group?.id}>
<TableCell>
<IDField value={group?.id} />
</TableCell>
<TableCell>{group?.name}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Section>
);
};
export default GroupDetails;

View file

@ -0,0 +1,35 @@
import CsrfInput from "./CsrfInput";
import Logo from "./Logo";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Toolbar from "@mui/material/Toolbar";
import React, { FC } from "react";
type HeaderProps = {
csrfToken: string;
signOutUrl: string;
};
const Header: FC<HeaderProps> = ({ csrfToken, signOutUrl }) => {
return (
<AppBar position="sticky">
<Toolbar>
<a href="/.pomerium">
<Logo />
</a>
<Box flexGrow={1} />
{signOutUrl ? (
<form action={signOutUrl}>
<CsrfInput csrfToken={csrfToken} />
<Button variant="text" color="inherit" type="submit">
Logout
</Button>
</form>
) : (
<></>
)}
</Toolbar>
</AppBar>
);
};
export default Header;

View file

@ -0,0 +1,24 @@
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import React, { FC } from "react";
export type HeroSectionProps = {
icon?: React.ReactNode;
title: React.ReactNode;
text?: React.ReactNode;
};
export const HeroSection: FC<HeroSectionProps> = ({ icon, title, text }) => {
return (
<Paper sx={{ padding: "16px" }}>
<Stack direction="row" spacing={2}>
{icon}
<Stack>
<Typography variant="h1">{title}</Typography>
{text ? <Typography>{text}</Typography> : <></>}
</Stack>
</Stack>
</Paper>
);
};
export default HeroSection;

View file

@ -0,0 +1,19 @@
import Box from "@mui/material/Box";
import React, { FC } from "react";
export type IDFieldProps = {
value: string;
};
export const IDField: FC<IDFieldProps> = ({ value }) => {
return (
<Box component="span" sx={{ fontFamily: '"DM Mono"', fontSize: "12px" }}>
{value?.split("")?.map((str, idx) => (
<React.Fragment key={idx}>
{str}
<wbr />
</React.Fragment>
))}
</Box>
);
};
export default IDField;

View file

@ -0,0 +1,59 @@
import createSvgIcon from "@mui/material/utils/createSvgIcon";
import React from "react";
const JwtIcon = createSvgIcon(
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 15 15"
>
<path
style={{
stroke: "none",
fillRule: "evenodd",
fill: "#fff",
fillOpacity: 1
}}
d="M8.586 4.04 8.57.042H6.34l.015 3.996L7.47 5.57ZM6.355 10.887v4.008h2.23v-4.008L7.47 9.355Zm0 0"
/>
<path
style={{
stroke: "none",
fillRule: "evenodd",
fill: "#00f2e6",
fillOpacity: 1
}}
d="m8.586 10.887 2.344 3.238 1.797-1.309-2.344-3.238L8.586 9ZM6.355 4.04 3.996.8 2.2 2.11l2.344 3.238 1.812.578Zm0 0"
/>
<path
style={{
stroke: "none",
fillRule: "evenodd",
fill: "#00b9f1",
fillOpacity: 1
}}
d="m4.543 5.348-3.8-1.235-.684 2.11 3.804 1.246 1.797-.594ZM9.266 8.05l1.117 1.528 3.8 1.235.684-2.11-3.805-1.234Zm0 0"
/>
<path
style={{
stroke: "none",
fillRule: "evenodd",
fill: "#d63aff",
fillOpacity: 1
}}
d="m11.063 7.469 3.804-1.246-.683-2.11-3.801 1.235-1.117 1.527ZM3.863 7.469.06 8.703l.683 2.11 3.801-1.235L5.66 8.051Zm0 0"
/>
<path
style={{
stroke: "none",
fillRule: "evenodd",
fill: "#fb015b",
fillOpacity: 1
}}
d="m4.543 9.578-2.344 3.238 1.797 1.309 2.36-3.238V9ZM10.383 5.348l2.344-3.239L10.93.801 8.586 4.039v1.887Zm0 0"
/>
</svg>,
"JWT"
);
export default JwtIcon;

View file

@ -0,0 +1,9 @@
import LogoURL from "../static/logo_white.svg";
import React from "react";
import type { FC } from "react";
const Logo: FC = () => {
return <img alt="Logo" src={LogoURL} height="30px" />;
};
export default Logo;

View file

@ -0,0 +1,10 @@
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
import React, { FC } from "react";
import { User } from "react-feather";
export const PersonIcon: FC<SvgIconProps> = (props) => (
<SvgIcon {...props}>
<User />
</SvgIcon>
);
export default PersonIcon;

View file

@ -0,0 +1,41 @@
import SectionFooter from "./SectionFooter";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import React, { FC } from "react";
export type SectionProps = React.PropsWithChildren<{
title: React.ReactNode;
icon?: React.ReactNode;
footer?: React.ReactNode;
}>;
export const Section: FC<SectionProps> = ({
title,
icon,
children,
footer
}) => {
return (
<Paper sx={{ overflow: "hidden" }}>
<Stack>
<Toolbar>
<Typography variant="h4" flexGrow={1}>
{title}
</Typography>
{icon ? <Box>{icon}</Box> : <></>}
</Toolbar>
<Box sx={{ padding: 3, paddingTop: 0 }}>{children}</Box>
{footer ? (
<SectionFooter>
<Typography variant="caption">{footer}</Typography>
</SectionFooter>
) : (
<></>
)}
</Stack>
</Paper>
);
};
export default Section;

View file

@ -0,0 +1,9 @@
import Box from "@mui/material/Box";
import styled from "@mui/material/styles/styled";
import React, { FC } from "react";
export const SectionFooter = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.grey[100],
padding: theme.spacing(3)
}));
export default SectionFooter;

View file

@ -0,0 +1,47 @@
import { Session } from "../types";
import ClaimsTable from "./ClaimsTable";
import IDField from "./IDField";
import Section from "./Section";
import Stack from "@mui/material/Stack";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import React, { FC } from "react";
export type SessionDetailsProps = {
session: Session;
};
export const SessionDetails: FC<SessionDetailsProps> = ({ session }) => {
return (
<Section title="Session Details">
<Stack spacing={3}>
<TableContainer>
<Table size="small">
<TableBody>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>
<IDField value={session?.id} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>User ID</TableCell>
<TableCell>
<IDField value={session?.userId} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Expires At</TableCell>
<TableCell>{session?.expiresAt || ""}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<ClaimsTable claims={session?.claims} />
</Stack>
</Section>
);
};
export default SessionDetails;

View file

@ -0,0 +1,81 @@
import DeviceCredentialsTable from "../components/DeviceCredentialsTable";
import SectionFooter from "../components/SectionFooter";
import { Session, User } from "../types";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import React, { FC } from "react";
export type SessionDeviceCredentialsProps = {
csrfToken: string;
user: User;
session: Session;
webAuthnUrl: string;
};
export const SessionDeviceCredentials: FC<SessionDeviceCredentialsProps> = ({
csrfToken,
user,
session,
webAuthnUrl
}) => {
const currentSessionDeviceCredentialIds = [];
const otherDeviceCredentialIds = [];
user?.deviceCredentialIds?.forEach((id) => {
if (session?.deviceCredentials?.find((cred) => cred?.id === id)) {
currentSessionDeviceCredentialIds.push(id);
} else {
otherDeviceCredentialIds.push(id);
}
});
return (
<Paper sx={{ overflow: "hidden" }}>
<Stack>
<Toolbar>
<Typography variant="h4" flexGrow={1}>
Current Session Device Credentials
</Typography>
</Toolbar>
<Box sx={{ padding: 3, paddingTop: 0 }}>
<DeviceCredentialsTable
csrfToken={csrfToken}
ids={currentSessionDeviceCredentialIds}
webAuthnUrl={webAuthnUrl}
/>
</Box>
{otherDeviceCredentialIds?.length > 0 ? (
<>
<Toolbar>
<Typography variant="h4" flexGrow={1}>
Other Device Credentials
</Typography>
</Toolbar>
<Box sx={{ padding: 3, paddingTop: 0 }}>
<DeviceCredentialsTable
csrfToken={csrfToken}
ids={otherDeviceCredentialIds}
webAuthnUrl={webAuthnUrl}
/>
</Box>
</>
) : (
<></>
)}
<SectionFooter>
<Typography variant="caption">
Register device with <a href={webAuthnUrl}>WebAuthn</a>.
</Typography>
</SectionFooter>
</Stack>
</Paper>
);
};
export default SessionDeviceCredentials;

View file

@ -0,0 +1,17 @@
import { User } from "../types";
import ClaimsTable from "./ClaimsTable";
import JwtIcon from "./JwtIcon";
import Section from "./Section";
import React, { FC } from "react";
export type UserClaimsProps = {
user: User;
};
export const UserClaims: FC<UserClaimsProps> = ({ user }) => {
return (
<Section title="User Claims" icon={<JwtIcon />}>
<ClaimsTable claims={user?.claims} />
</Section>
);
};
export default UserClaims;

View file

@ -0,0 +1,55 @@
import GroupDetails from "./GroupDetails";
import HeroSection from "./HeroSection";
import PersonIcon from "./PersonIcon";
import SessionDetails from "./SessionDetails";
import SessionDeviceCredentials from "./SessionDeviceCredentials";
import UserClaims from "./UserClaims";
import MuiAvatar from "@mui/material/Avatar";
import Container from "@mui/material/Container";
import Stack from "@mui/material/Stack";
import styled from "@mui/material/styles/styled";
import React, { FC } from "react";
import { UserInfoPageData } from "src/types";
const Avatar = styled(MuiAvatar)(({ theme }) => ({
backgroundColor: theme.palette.primary.main,
height: 48,
width: 48
}));
type UserInfoPageProps = {
data: UserInfoPageData;
};
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
const name = data?.user?.claims?.given_name?.[0] || data?.user?.name;
return (
<Container>
<Stack spacing={3}>
<HeroSection
icon={
<Avatar>
<PersonIcon />
</Avatar>
}
title={<>Hi {name}!</>}
text={
<>
Welcome to the user info endpoint. Here you can view your current
session details, and authorization context.
</>
}
/>
<SessionDetails session={data?.session} />
<UserClaims user={data?.user} />
<GroupDetails groups={data?.directoryGroups} />
<SessionDeviceCredentials
csrfToken={data?.csrfToken}
session={data?.session}
user={data?.user}
webAuthnUrl={data?.webAuthnUrl}
/>
</Stack>
</Container>
);
};
export default UserInfoPage;

View file

@ -0,0 +1,210 @@
import { decode, encodeUrl } from "../util/base64";
import AlertDialog from "./AlertDialog";
import ExperimentalIcon from "./ExperimentalIcon";
import HeroSection from "./HeroSection";
import Button from "@mui/material/Button";
import Container from "@mui/material/Container";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import React, { FC, useRef, useState } from "react";
import {
WebAuthnCreationOptions,
WebAuthnRegistrationPageData,
WebAuthnRequestOptions
} from "src/types";
type CredentialForAuthenticate = {
id: string;
type: string;
rawId: ArrayBuffer;
response: {
authenticatorData: ArrayBuffer;
clientDataJSON: ArrayBuffer;
signature: ArrayBuffer;
userHandle: ArrayBuffer;
};
};
async function authenticateCredential(
requestOptions: WebAuthnRequestOptions
): Promise<CredentialForAuthenticate> {
const credential = await navigator.credentials.get({
publicKey: {
allowCredentials: requestOptions?.allowCredentials?.map((c) => ({
type: c.type,
id: decode(c.id)
})),
challenge: decode(requestOptions?.challenge),
timeout: requestOptions?.timeout,
userVerification: requestOptions?.userVerification
}
});
return credential as CredentialForAuthenticate;
}
type CredentialForCreate = {
id: string;
type: string;
rawId: ArrayBuffer;
response: {
attestationObject: ArrayBuffer;
clientDataJSON: ArrayBuffer;
};
};
async function createCredential(
creationOptions: WebAuthnCreationOptions
): Promise<CredentialForCreate> {
const credential = await navigator.credentials.create({
publicKey: {
attestation: creationOptions?.attestation || undefined,
authenticatorSelection: {
authenticatorAttachment:
creationOptions?.authenticatorSelection?.authenticatorAttachment ||
undefined,
requireResidentKey:
creationOptions?.authenticatorSelection?.requireResidentKey ||
undefined,
residentKey: creationOptions?.authenticatorSelection?.residentKey,
userVerification:
creationOptions?.authenticatorSelection?.userVerification || undefined
},
challenge: decode(creationOptions?.challenge),
pubKeyCredParams: creationOptions?.pubKeyCredParams?.map((p) => ({
type: p.type,
alg: p.alg
})),
rp: {
name: creationOptions?.rp?.name
},
timeout: creationOptions?.timeout,
user: {
id: decode(creationOptions?.user?.id),
name: creationOptions?.user?.name,
displayName: creationOptions?.user?.displayName
}
}
});
return credential as CredentialForCreate;
}
type WebAuthnRegistrationPageProps = {
data: WebAuthnRegistrationPageData;
};
const WebAuthnRegistrationPage: FC<WebAuthnRegistrationPageProps> = ({
data
}) => {
const authenticateFormRef = useRef<HTMLFormElement>();
const authenticateResponseRef = useRef<HTMLInputElement>();
const registerFormRef = useRef<HTMLFormElement>();
const registerResponseRef = useRef<HTMLInputElement>();
const [error, setError] = useState<string>(null);
const enableAuthenticate = data?.requestOptions?.allowCredentials?.length > 0;
function handleClickAuthenticate(evt: React.MouseEvent): void {
evt.preventDefault();
void (async () => {
try {
const credential = await authenticateCredential(data?.requestOptions);
authenticateResponseRef.current.value = JSON.stringify({
id: credential.id,
type: credential.type,
rawId: encodeUrl(credential.rawId),
response: {
authenticatorData: encodeUrl(credential.response.authenticatorData),
clientDataJSON: encodeUrl(credential.response.clientDataJSON),
signature: encodeUrl(credential.response.signature),
userHandle: encodeUrl(credential.response.userHandle)
}
});
authenticateFormRef.current.submit();
} catch (e) {
setError(`${e}`);
}
})();
}
function handleClickDialogOK(evt: React.MouseEvent): void {
evt.preventDefault();
setError(null);
}
function handleClickRegister(evt: React.MouseEvent): void {
evt.preventDefault();
void (async () => {
try {
const credential = await createCredential(data?.creationOptions);
registerResponseRef.current.value = JSON.stringify({
id: credential.id,
type: credential.type,
rawId: encodeUrl(credential.rawId),
response: {
attestationObject: encodeUrl(credential.response.attestationObject),
clientDataJSON: encodeUrl(credential.response.clientDataJSON)
}
});
registerFormRef.current.submit();
} catch (e) {
setError(`${e}`);
}
})();
}
return (
<Container>
<Stack spacing={3}>
<HeroSection
title={
<>
WebAuthn Registration <ExperimentalIcon />
</>
}
/>
<Paper sx={{ padding: "16px" }}>
<Stack direction="row" justifyContent="center" spacing={3}>
<Button onClick={handleClickRegister} variant="contained">
Register New Device
</Button>
<Button
onClick={handleClickAuthenticate}
variant="contained"
disabled={!enableAuthenticate}
>
Authenticate Existing Device
</Button>
</Stack>
</Paper>
<form ref={authenticateFormRef} method="post" action={data?.selfUrl}>
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
<input type="hidden" name="action" value="authenticate" />
<input
type="hidden"
name="authenticate_response"
ref={authenticateResponseRef}
/>
</form>
<form ref={registerFormRef} method="POST" action={data?.selfUrl}>
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
<input type="hidden" name="action" value="register" />
<input
type="hidden"
name="register_response"
ref={registerResponseRef}
/>
</form>
</Stack>
<AlertDialog
title="Error"
severity="error"
open={!!error}
actions={<Button onClick={handleClickDialogOK}>OK</Button>}
>
{error}
</AlertDialog>
</Container>
);
};
export default WebAuthnRegistrationPage;

1
ui/src/globals.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module "*.svg";

5
ui/src/index.tsx Normal file
View file

@ -0,0 +1,5 @@
import App from "./App";
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));

View file

@ -0,0 +1,23 @@
<svg width="161" height="28" viewBox="0 0 161 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M5.71009 27.3796C5.13899 27.3067 4.37349 27.6226 4.03326 27.2216C3.70518 26.8328 3.9482 26.0916 3.93605 25.5083C3.93605 21.1583 3.93605 16.8083 3.9482 12.4461C3.9482 11.3525 3.76594 10.3075 3.10979 9.39618C2.47794 8.52132 1.62737 7.92592 0.485183 7.87732C0.0842016 7.86516 -0.0130059 7.71935 -0.000854965 7.34267C0.0234469 6.67437 0.0234469 6.00607 -0.000854965 5.33777C-0.0130059 4.99754 0.0720507 4.82743 0.436579 4.79098C2.36858 4.59656 4.30058 4.37784 6.22043 4.15913C8.97869 3.85535 11.7248 3.53943 14.4831 3.23566C17.1927 2.93188 19.9024 2.64026 22.6242 2.33649C25.2974 2.04486 27.9706 1.75324 30.6438 1.44947C33.5965 1.12139 36.5492 0.781165 39.5019 0.45309C39.5748 0.440939 39.6477 0.489543 39.7206 0.513845C39.9636 0.732562 40.0122 1.02418 40.0244 1.34011C40.0244 2.00841 40.0365 2.67671 40.0244 3.34501C40.0001 4.23203 39.915 4.34139 39.0766 4.57226C38.3475 4.76667 37.6428 5.00969 37.0231 5.44713C35.2733 6.66222 34.3377 8.3269 34.3256 10.4655C34.3013 15.9091 34.3256 21.3406 34.3134 26.7842C34.3134 27.1973 34.4106 27.6469 34.0218 27.975C32.8067 27.9628 31.5916 27.9385 30.3765 27.9507C29.8783 27.9628 29.7811 27.7684 29.7811 27.3188C29.7933 21.7537 29.7811 16.1886 29.7933 10.6234C29.7933 9.05596 29.2951 7.74366 28.1164 6.68652C27.8856 6.49211 27.6668 6.28554 27.3631 6.20049C26.4396 6.00607 25.5161 5.7995 24.5684 5.90886C22.284 6.16403 20.3034 8.08388 20.0117 10.3561C19.9145 11.073 19.8659 11.8021 19.8659 12.5311C19.8781 17.2214 19.8781 21.9117 19.8659 26.6019C19.8659 26.9907 19.9753 27.4282 19.55 27.7077C19.4771 27.7077 19.4164 27.7077 19.3434 27.7077C18.5658 27.5861 17.7881 27.7441 17.0105 27.6226C15.9655 27.5861 15.9655 27.5861 15.9655 26.5533C15.9655 21.5593 15.9533 16.5652 15.9655 11.5834C15.9655 10.0037 15.4794 8.65498 14.2279 7.6343C14.1185 7.54924 13.9849 7.48849 13.8634 7.42773C12.5997 7.13611 11.3238 6.88094 10.0966 7.53709C8.37115 8.43626 7.50843 9.93083 7.48413 11.8628C7.44767 16.5166 7.47198 21.1583 7.47198 25.8121C7.47198 26.1159 7.47198 26.4197 7.45983 26.7234C7.44767 26.9786 7.38692 27.1973 7.1682 27.3674C6.68216 27.4768 6.19613 27.4282 5.71009 27.3796Z" fill="white"/>
<path d="M79.5888 14.7074C79.686 17.1619 79.443 19.6164 79.7468 22.0952C78.6775 22.0952 77.5353 22.0952 76.4053 22.0952C76.709 20.1511 76.5389 18.1826 76.5389 16.2142C76.5389 14.2457 76.6969 12.2651 76.4053 10.2237C77.7176 10.2237 79.1271 10.2237 80.5244 10.2237C80.6217 10.2237 80.731 10.2237 80.731 10.3695C80.731 11.0743 81.1684 11.6211 81.4479 12.2165C82.3106 14.0634 83.1855 15.9104 84.0604 17.7452C84.1454 17.9153 84.1697 18.134 84.4128 18.2677C84.7287 17.6237 85.0324 17.004 85.3362 16.3721C86.1746 14.6224 87.0738 12.9091 87.8029 11.1108C87.9122 10.8434 87.8879 10.3817 88.058 10.2845C88.3497 10.1387 88.7628 10.2116 89.1273 10.2116C90.1844 10.2116 91.2294 10.2116 92.2866 10.2116C92.323 10.2116 92.3595 10.2359 92.4202 10.248C92.2015 10.6733 92.1894 11.1472 92.1894 11.6089C92.1894 14.671 92.1894 17.7209 92.1894 20.7829C92.1894 21.2203 92.2015 21.6699 92.4202 22.1074C91.2537 22.1074 90.1237 22.1074 88.9937 22.1074C89.1759 21.2689 89.2731 15.8618 89.1152 14.7196C88.9086 14.7439 88.9086 14.9383 88.8478 15.072C87.8636 16.9675 86.8915 18.8752 86.0045 20.8194C85.9316 20.9895 85.798 21.1596 85.798 21.3175C85.8223 22.241 85.1904 22.0952 84.595 22.1438C83.6472 22.2046 83.0883 22.1074 82.6873 20.9652C81.9097 18.7902 80.7675 16.7488 79.7832 14.6588C79.7103 14.6953 79.6496 14.6953 79.5888 14.7074Z" fill="white"/>
<path d="M153.138 18.3527C154.317 15.8496 155.641 13.5044 156.565 10.9892C156.82 10.2966 157.112 10.2115 157.707 10.2237C158.837 10.2601 159.967 10.2358 160.988 10.2358C160.915 14.1971 160.915 18.134 161 22.1195C160.016 22.1195 158.898 22.1195 157.78 22.1195C158.072 19.6771 157.829 17.2105 157.938 14.7438C157.683 14.7438 157.683 14.9018 157.634 14.999C156.699 16.8824 155.763 18.7537 154.84 20.6371C154.742 20.8315 154.633 21.038 154.572 21.2446C154.487 21.5241 154.499 21.998 154.341 22.0709C154.013 22.2167 153.6 22.1438 153.211 22.1559C152.786 22.1559 152.312 22.2653 151.948 22.1195C151.583 21.9737 151.717 21.4269 151.547 21.0745C150.526 18.9481 149.53 16.8095 148.448 14.6952C148.424 17.174 148.242 19.6285 148.485 22.1316C147.427 22.1316 146.285 22.1316 145.265 22.1316C145.35 18.1947 145.362 14.2457 145.252 10.248C146.48 10.248 147.865 10.248 149.25 10.248C149.42 10.248 149.481 10.2844 149.493 10.4789C149.517 10.7583 149.602 11.0378 149.724 11.293C150.793 13.5774 151.887 15.8496 152.968 18.134C152.993 18.1583 153.041 18.2069 153.138 18.3527Z" fill="white"/>
<path d="M95.2515 10.2358C96.3694 10.2358 97.633 10.2358 98.9089 10.2358C101.23 10.2358 103.563 10.2479 105.884 10.2236C106.357 10.2236 106.54 10.2965 106.564 10.8312C106.588 11.5724 106.771 12.3014 106.892 13.1034C105.738 12.7632 104.62 12.5445 103.478 12.4716C101.947 12.3743 100.403 12.4351 98.8724 12.4351C98.5444 12.4351 98.3986 12.5931 98.3986 12.9211C98.4107 13.4679 98.4107 14.0147 98.3986 14.5615C98.3864 14.9625 98.5687 15.1083 98.9453 15.0962C100.877 15.0354 102.822 15.2906 104.766 14.7681C104.656 15.5336 104.547 16.2627 104.462 17.0039C104.425 17.3562 104.17 17.2833 103.964 17.2833C102.36 17.2833 100.768 17.3076 99.1641 17.2712C98.5322 17.259 98.3378 17.4777 98.3864 18.0731C98.4229 18.5592 98.3986 19.0452 98.3986 19.5313C98.3986 19.7864 98.4958 19.9565 98.7874 19.9565C101.108 19.9079 103.441 20.078 105.762 19.8593C106.297 19.8107 106.758 19.5556 107.293 19.2639C107.159 20.2239 107.062 21.123 106.916 21.9979C106.868 22.2774 106.576 22.1437 106.406 22.1437C103.794 22.1559 101.181 22.1437 98.5808 22.1437C97.4265 22.1437 96.2843 22.1437 95.2636 22.1437C95.3365 18.1582 95.3365 14.2091 95.2515 10.2358Z" fill="white"/>
<path d="M130.112 10.248C131.267 10.248 132.397 10.248 133.527 10.248C133.259 11.6697 133.43 13.1157 133.393 14.5495C133.369 15.5216 133.381 16.4936 133.405 17.4657C133.454 19.2154 134.219 20.0053 135.933 20.1632C136.479 20.2118 137.026 20.1511 137.561 20.0174C138.472 19.7987 139.08 19.0332 139.104 17.8667C139.153 15.558 139.128 13.2615 139.116 10.9528C139.116 10.7219 139.019 10.4911 138.958 10.248C140.088 10.248 141.218 10.248 142.397 10.248C142.008 11.3052 142.202 12.3988 142.166 13.468C142.117 15.0598 142.312 16.6637 142.069 18.2555C141.729 20.467 140.392 21.7793 138.205 22.2046C136.735 22.4962 135.264 22.4719 133.818 22.0952C131.728 21.5484 130.416 19.8351 130.367 17.5872C130.331 15.6431 130.355 13.6989 130.355 11.7669C130.367 11.2444 130.331 10.7462 130.112 10.248Z" fill="white"/>
<path d="M34.0225 27.9636C34.0225 22.1433 34.0346 16.323 34.0346 10.4906C34.0346 7.42853 36.0517 4.91329 39.053 4.26929C39.539 4.15993 39.7213 3.98982 39.7091 3.47948C39.6726 2.49525 39.7213 1.49887 39.7334 0.514648C40.3045 0.575403 40.8148 0.830573 41.3616 0.964233C41.629 1.02499 41.7019 1.2194 41.6897 1.46242C41.6897 2.4102 41.6776 3.37012 41.6897 4.31789C41.7019 4.77963 41.3981 4.81608 41.07 4.88899C38.8342 5.44793 37.2425 6.74808 36.4283 8.93525C36.1732 9.62786 36.076 10.3448 36.076 11.0738C36.0881 16.4567 36.076 21.8396 36.076 27.2224C36.076 28.0001 36.076 28.0001 35.2862 27.9636C34.873 27.9758 34.4477 27.9758 34.0225 27.9636Z" fill="#C5B7DD"/>
<path d="M19.5508 27.6962C19.648 27.4167 19.6237 27.1373 19.6237 26.8578C19.6237 21.7544 19.6358 16.651 19.6237 11.5476C19.6237 9.53053 20.3284 7.878 21.9202 6.62646C23.5363 5.36276 25.7721 5.19264 27.376 6.18902C24.4476 6.07966 21.665 8.40049 21.6529 12.1065C21.6407 17.0398 21.6529 21.961 21.6529 26.8942C21.6529 27.7326 21.6529 27.7326 20.8266 27.7205C20.4013 27.7083 19.9761 27.7083 19.5508 27.6962Z" fill="#C5B7DD"/>
<path d="M7.16896 27.3558C7.1568 25.1079 7.14465 22.8721 7.14465 20.6242C7.14465 17.6958 7.12035 14.7553 7.15681 11.8269C7.18111 9.49392 8.74858 7.41611 10.875 6.91792C11.9321 6.66275 12.965 6.74781 13.8884 7.41611C11.1545 7.50116 9.56269 9.72479 9.25892 11.8998C9.17386 12.5438 9.14956 13.1878 9.14956 13.8318C9.16171 18.1089 9.14956 22.3982 9.14956 26.6754C9.14956 27.4652 9.14956 27.4652 8.38405 27.4409C7.98307 27.4409 7.58209 27.3923 7.16896 27.3558Z" fill="#C5B7DD"/>
<path d="M127.463 22.1312C126.26 22.1312 125.142 22.1312 124.158 22.1312C124.207 18.17 124.207 14.2331 124.158 10.2598C125.142 10.2598 126.285 10.2598 127.475 10.2598C127.208 10.7701 127.22 11.3169 127.22 11.8515C127.22 14.7678 127.22 17.684 127.22 20.6002C127.208 21.1106 127.22 21.6087 127.463 22.1312Z" fill="white"/>
<path d="M17.0112 27.624C17.7889 27.5875 18.5665 27.5754 19.3442 27.709C18.5665 27.7576 17.7767 27.7819 17.0112 27.624Z" fill="#C5B7DD"/>
<path d="M5.71094 27.3807C6.19697 27.3686 6.68301 27.3686 7.16905 27.3564C6.69516 27.6116 6.19697 27.4658 5.71094 27.3807Z" fill="#C5B7DD"/>
<path d="M122.141 22.107C120.683 20.5638 119.893 18.4495 117.816 17.4531C117.998 17.3681 118.107 17.3195 118.216 17.2709C119.699 16.639 120.477 15.351 120.343 13.7471C120.185 11.8516 119.432 10.8187 117.682 10.4664C114.887 9.90741 112.044 10.3691 109.371 10.199C109.541 14.2088 109.492 18.1457 109.395 22.107C110.416 22.107 111.558 22.107 112.7 22.107C112.348 20.8676 112.566 19.6282 112.518 18.4009C112.506 18.0485 112.773 17.9999 113.064 17.9878C115.191 17.8541 116.698 18.8019 117.658 20.8676C117.852 21.2685 117.986 21.6695 117.779 22.107C119.261 22.107 120.707 22.107 122.141 22.107ZM116.236 15.8006C115.106 16.1165 113.952 15.9464 112.797 15.9343C112.518 15.9343 112.506 15.7034 112.506 15.4968C112.506 14.6463 112.506 13.7957 112.506 12.9573C112.506 12.6535 112.627 12.5077 112.931 12.5077C113.417 12.5077 113.903 12.5077 114.389 12.5077C114.389 12.5199 114.389 12.532 114.389 12.532C114.899 12.532 115.397 12.4956 115.896 12.5442C116.758 12.6171 117.22 13.1031 117.317 13.9294C117.415 14.8164 117.014 15.5819 116.236 15.8006Z" fill="white"/>
<path d="M73.7928 14.1721C73.2825 11.9363 71.8608 10.6483 69.6372 10.2352C68.0333 9.93141 66.4172 9.93141 64.8133 10.2716C62.7112 10.7212 61.3138 11.9728 60.7913 14.087C60.4511 15.4601 60.4511 16.8574 60.7913 18.2305C61.3138 20.3448 62.6018 21.6935 64.7768 22.1431C65.6152 22.3132 66.4536 22.3983 67.3771 22.374C67.9847 22.4104 68.6408 22.3375 69.3091 22.2525C71.5327 21.9851 73.1488 20.6364 73.7199 18.4857C74.1087 17.064 74.133 15.6181 73.7928 14.1721ZM70.7308 18.1454C70.4756 19.0203 69.9288 19.64 69.0539 19.9073C67.8389 20.284 66.6238 20.284 65.433 19.8344C64.6918 19.555 64.1693 19.0325 63.9262 18.2791C63.4888 16.9425 63.4645 15.5816 63.8776 14.2328C64.145 13.3337 64.7404 12.7383 65.6274 12.4588C66.6359 12.1429 67.6687 12.1307 68.6773 12.3616C70.1111 12.6897 70.8158 13.6496 70.9616 15.3264C70.9859 15.6059 70.9616 15.8975 70.9616 16.177C70.9981 16.8453 70.913 17.5014 70.7308 18.1454Z" fill="white"/>
<path d="M58.689 13.4924C58.4945 11.9979 57.5832 10.9043 56.1373 10.5033C55.4933 10.321 54.8371 10.236 54.1567 10.236C52.1518 10.236 50.1469 10.236 48.142 10.236C47.9719 10.236 47.7896 10.1752 47.6924 10.236C48.0326 12.253 47.8382 14.2336 47.8503 16.2021C47.8503 18.1706 48.0205 20.1512 47.7167 22.1196C48.8346 22.1196 49.9646 22.1196 51.1432 22.1196C50.7544 21.026 50.9853 19.9689 50.9124 18.9239C50.8759 18.4743 51.0096 18.3407 51.4713 18.3407C52.8079 18.3407 54.1445 18.4257 55.469 18.2435C57.1336 18.0126 58.3609 16.919 58.6525 15.2908C58.7619 14.6954 58.774 14.1 58.689 13.4924ZM54.7035 15.9226C53.5856 16.2872 52.4312 16.1899 51.2769 16.1656C50.961 16.1535 50.9124 15.8983 50.9124 15.631C50.9124 15.1814 50.9124 14.744 50.9124 14.2944C50.9124 13.8448 50.9124 13.4074 50.9124 12.9578C50.9124 12.7148 50.9731 12.5082 51.2769 12.5082C52.3826 12.4839 53.5005 12.3867 54.5941 12.6662C55.3353 12.8606 55.6755 13.3466 55.7241 14.185C55.7727 15.0599 55.4204 15.6918 54.7035 15.9226Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="161" height="27.5462" fill="white" transform="translate(0 0.454102)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

161
ui/src/theme/index.ts Normal file
View file

@ -0,0 +1,161 @@
import { softShadows } from "./shadows";
import "@fontsource/dm-mono";
import "@fontsource/dm-sans";
import common from "@mui/material/colors/common";
import muiCreateTheme, {
Theme as MuiTheme,
} from "@mui/material/styles/createTheme";
export const createTheme = (): MuiTheme =>
muiCreateTheme({
components: {
MuiBackdrop: {
styleOverrides: {
root: {
backgroundColor: "rgba(68, 56, 102, 0.8)",
},
},
},
MuiBreadcrumbs: {
styleOverrides: {
separator: {
opacity: "30%",
},
},
},
MuiChip: {
styleOverrides: {
root: {
backgroundColor: "rgba(0,0,0,0.075)",
},
},
},
MuiDialog: {
styleOverrides: {
paper: {
padding: 0,
},
},
},
MuiDialogActions: {
styleOverrides: {
root: {
padding: "16px",
display: "flex",
flexFlow: "row nowrap",
justifyContent: "flex-end",
},
},
},
MuiDialogContent: {
styleOverrides: {
root: { padding: "16px" },
},
},
MuiDialogTitle: {
styleOverrides: {
root: {
display: "flex",
flexFlow: "row nowrap",
justifyContent: "space-between",
alignItems: "center",
padding: "16px",
},
},
},
MuiFilledInput: {
styleOverrides: {
root: {
borderRadius: "4px",
},
},
},
MuiLinearProgress: {
styleOverrides: {
root: {
borderRadius: 3,
overflow: "hidden",
},
},
},
MuiListItemIcon: {
styleOverrides: {
root: {
minWidth: 32,
},
},
},
MuiOutlinedInput: {
styleOverrides: {
root: {
borderRadius: "4px",
},
},
},
MuiTableCell: {
styleOverrides: {
head: {
fontWeight: 600,
},
},
},
},
palette: {
action: {
active: "#39256C",
},
background: {
default: "#FBFBFB",
paper: common.white,
},
primary: {
main: "#6F43E7",
},
secondary: {
main: "#49AAA1",
},
},
shadows: softShadows,
shape: {
borderRadius: "16px",
},
typography: {
fontFamily: [
'"DM Sans"',
"-apple-system",
"BlinkMacSystemFont",
'"Segoe UI"',
"Roboto",
'"Helvetica Neue"',
"Arial",
"sans-serif",
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(","),
h1: {
fontSize: "3.052rem",
fontWeight: 550,
},
h2: {
fontSize: "2.441rem",
fontWeight: 550,
},
h3: {
fontSize: "1.953rem",
fontWeight: 550,
},
h4: {
fontSize: "1.563rem",
fontWeight: 550,
},
h5: {
fontSize: "1.25rem",
fontWeight: 550,
},
h6: {
fontSize: "1rem",
fontWeight: 550,
},
},
});

57
ui/src/theme/shadows.ts Normal file
View file

@ -0,0 +1,57 @@
import type { Shadows } from "@mui/material/styles/shadows";
export const softShadows: Shadows = [
"none",
"0 0 0 1px rgba(63,63,68,0.05), 0 1px 2px 0 rgba(63,63,68,0.15)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 2px 2px -2px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 3px 4px -2px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 3px 4px -2px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 4px 6px -2px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 4px 6px -2px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 4px 8px -2px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 5px 8px -2px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 6px 12px -4px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 7px 12px -4px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 6px 16px -4px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 7px 16px -4px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 8px 18px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 9px 18px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 10px 20px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 11px 20px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 12px 22px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 13px 22px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 14px 24px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 16px 28px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 18px 30px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 20px 32px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 22px 34px -8px rgba(0,0,0,0.25)",
"0 0 1px 0 rgba(0,0,0,0.31), 0 24px 36px -8px rgba(0,0,0,0.25)",
];
export const strongShadows: Shadows = [
"none",
"0 0 1px 0 rgba(0,0,0,0.70), 0 3px 4px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 2px 2px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 3px 4px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 3px 4px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 4px 6px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 4px 6px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 4px 8px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 5px 8px -2px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 6px 12px -4px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 7px 12px -4px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 6px 16px -4px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 7px 16px -4px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 8px 18px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 9px 18px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 10px 20px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 11px 20px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 12px 22px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 13px 22px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 14px 24px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 16px 28px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 18px 30px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 20px 32px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 22px 34px -8px rgba(0,0,0,0.50)",
"0 0 1px 0 rgba(0,0,0,0.70), 0 24px 36px -8px rgba(0,0,0,0.50)",
];

127
ui/src/types/index.ts Normal file
View file

@ -0,0 +1,127 @@
export type Claims = Record<string, unknown[]>;
export type DirectoryUser = {
displayName: string;
email: string;
groupIds: string[];
id: string;
};
export type Group = {
id: string;
email: string;
name: string;
};
export type Session = {
audience: string[];
claims: Claims;
deviceCredentials: Array<{
typeId: string;
id: string;
}>;
expiresAt: string;
id: string;
idToken: {
expiresAt: string;
issuedAt: string;
issuer: string;
raw: string;
subject: string;
};
issuedAt: string;
oauthToken: {
accessToken: string;
expiresAt: string;
refreshToken: string;
tokenType: string;
};
userId: string;
};
export type User = {
claims: Claims;
deviceCredentialIds: string[];
id: string;
name: string;
};
export type WebAuthnCreationOptions = {
attestation: AttestationConveyancePreference;
authenticatorSelection: {
authenticatorAttachment?: AuthenticatorAttachment;
requireResidentKey?: boolean;
residentKey?: ResidentKeyRequirement;
userVerification?: UserVerificationRequirement;
};
challenge: string;
pubKeyCredParams: PublicKeyCredentialParameters[];
rp: {
name: string;
};
timeout: number;
user: {
displayName: string;
id: string;
name: string;
};
};
export type WebAuthnRequestOptions = {
allowCredentials: Array<{
type: "public-key";
id: string;
}>;
challenge: string;
timeout: number;
userVerification: UserVerificationRequirement;
};
// page data
type BasePageData = {
csrfToken?: string;
signOutUrl?: string;
};
export type ErrorPageData = BasePageData & {
page: "Error";
canDebug?: boolean;
debugUrl?: string;
error?: string;
requestId?: string;
status?: number;
statusText?: string;
version?: string;
};
export type DeviceEnrolledPageData = BasePageData & {
page: "DeviceEnrolled";
};
export type UserInfoPageData = BasePageData & {
page: "UserInfo";
csrfToken: string;
directoryGroups?: Group[];
directoryUser?: DirectoryUser;
session?: Session;
user?: User;
webAuthnUrl?: string;
};
export type WebAuthnRegistrationPageData = BasePageData & {
page: "WebAuthnRegistration";
creationOptions?: WebAuthnCreationOptions;
csrfToken: string;
requestOptions?: WebAuthnRequestOptions;
selfUrl: string;
};
export type PageData =
| ErrorPageData
| DeviceEnrolledPageData
| UserInfoPageData
| WebAuthnRegistrationPageData;

View file

@ -38,7 +38,11 @@ base64Lookup[95 /* _ */] = 63;
/** /**
* Encode an `ArrayBuffer` to base64. * Encode an `ArrayBuffer` to base64.
*/ */
function encode(buffer, chars = base64Chars, padding = "=") { export function encode(
buffer: ArrayBuffer,
chars = base64Chars,
padding = "="
): string {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
const length = bytes.length; const length = bytes.length;
let base64 = ""; let base64 = "";
@ -59,14 +63,14 @@ function encode(buffer, chars = base64Chars, padding = "=") {
/** /**
* Encode using the base64url variant. * Encode using the base64url variant.
*/ */
function encodeUrl(buffer) { export function encodeUrl(buffer: ArrayBuffer): string {
return encode(buffer, base64UrlChars, ""); return encode(buffer, base64UrlChars, "");
} }
/** /**
* Decode a base64 encoded string. * Decode a base64 encoded string.
*/ */
function decode(base64, lookup = base64Lookup) { export function decode(base64: string, lookup = base64Lookup): ArrayBuffer {
const length = base64.length; const length = base64.length;
let bufferLength = Math.floor(base64.length * 0.75); let bufferLength = Math.floor(base64.length * 0.75);
let p = 0; let p = 0;
@ -88,5 +92,3 @@ function decode(base64, lookup = base64Lookup) {
} }
return bytes; return bytes;
} }
export { encode, encodeUrl, decode };

22
ui/tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"strict": false,
"strictNullChecks": false,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"preserveConstEnums": true,
"noEmit": true,
"downlevelIteration": true,
"jsx": "react-jsx",
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

2472
ui/yarn.lock Normal file

File diff suppressed because it is too large Load diff

8995
yarn.lock Normal file

File diff suppressed because it is too large Load diff