diff --git a/.golangci.yml b/.golangci.yml index daa4d0595..dcf979fba 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,7 @@ linters-settings: lines: 100 statements: 50 gci: - local-prefixes: github.com/pomerium/pomerium + local-prefixes: github.com/pomerium goconst: min-len: 2 min-occurrences: 2 @@ -28,7 +28,7 @@ linters-settings: gocyclo: min-complexity: 15 goimports: - local-prefixes: github.com/pomerium/pomerium + local-prefixes: github.com/pomerium govet: check-shadowing: false lll: diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 07648647d..330769209 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -12,11 +12,11 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/pomerium/csrf" "github.com/rs/cors" "golang.org/x/oauth2" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/pomerium/csrf" "github.com/pomerium/pomerium/authenticate/handlers" "github.com/pomerium/pomerium/authenticate/handlers/webauthn" "github.com/pomerium/pomerium/internal/httputil" @@ -55,8 +55,7 @@ func (a *Authenticate) Mount(r *mux.Router) { csrf.Path("/"), csrf.UnsafePaths( []string{ - "/oauth2/callback", // rfc6749#section-10.12 accepts GET - "/.pomerium/sign_out", // https://openid.net/specs/openid-connect-frontchannel-1_0.html + "/oauth2/callback", // rfc6749#section-10.12 accepts GET }), csrf.FormValueName("state"), // rfc6749#section-10.12 csrf.CookieName(csrfKey), @@ -96,14 +95,10 @@ func (a *Authenticate) mountDashboard(r *mux.Router) { sr.Use(a.VerifySession) sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo)) sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn)) - sr.Path("/sign_out").Handler(a.requireValidSignature(a.SignOut)) + sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut)) sr.Path("/webauthn").Handler(webauthn.New(a.getWebauthnState)) 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) + handlers.DeviceEnrolled().ServeHTTP(w, r) return nil })) @@ -276,6 +271,25 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error { // SignOut signs the user out and attempts to revoke the user's identity session // Handles both GET and POST. func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error { + // check for an HMAC'd URL. If none is found, show a confirmation page. + err := middleware.ValidateRequestURL(a.getExternalRequest(r), a.state.Load().sharedKey) + if err != nil { + authenticateURL, err := a.options.Load().GetAuthenticateURL() + if err != nil { + return err + } + + handlers.SignOutConfirm(handlers.SignOutConfirmData{ + URL: urlutil.SignOutURL(r, authenticateURL, a.state.Load().sharedKey), + }).ServeHTTP(w, r) + return nil + } + + // otherwise actually do the sign out + return a.signOutRedirect(w, r) +} + +func (a *Authenticate) signOutRedirect(w http.ResponseWriter, r *http.Request) error { ctx, span := trace.StartSpan(r.Context(), "authenticate.SignOut") defer span.End() @@ -553,7 +567,6 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error { DirectoryUser: pbDirectoryUser, IsImpersonated: isImpersonated, Session: pbSession, - SignOutURL: urlutil.SignOutURL(r, authenticateURL, state.sharedKey), User: pbUser, WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()), }).ServeHTTP(w, r) diff --git a/authenticate/handlers/device-enrolled.go b/authenticate/handlers/device-enrolled.go index fcb3d885b..604be4af2 100644 --- a/authenticate/handlers/device-enrolled.go +++ b/authenticate/handlers/device-enrolled.go @@ -2,18 +2,14 @@ package handlers import ( "net/http" - "net/url" "github.com/pomerium/pomerium/internal/httputil" - "github.com/pomerium/pomerium/internal/urlutil" "github.com/pomerium/pomerium/ui" ) // DeviceEnrolled displays an HTML page informing the user that they've successfully enrolled a device. -func DeviceEnrolled(authenticateURL *url.URL, sharedKey []byte) http.Handler { +func DeviceEnrolled() http.Handler { return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { - return ui.ServePage(w, r, "DeviceEnrolled", map[string]interface{}{ - "signOutUrl": urlutil.SignOutURL(r, authenticateURL, sharedKey), - }) + return ui.ServePage(w, r, "DeviceEnrolled", map[string]interface{}{}) }) } diff --git a/authenticate/handlers/signout.go b/authenticate/handlers/signout.go new file mode 100644 index 000000000..db2b0aac3 --- /dev/null +++ b/authenticate/handlers/signout.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "net/http" + + "github.com/pomerium/pomerium/internal/httputil" + "github.com/pomerium/pomerium/ui" +) + +// SignOutConfirmData is the data for the SignOutConfirm page. +type SignOutConfirmData struct { + URL string +} + +// ToJSON converts the data into a JSON map. +func (data SignOutConfirmData) ToJSON() map[string]interface{} { + return map[string]interface{}{ + "url": data.URL, + } +} + +// SignOutConfirm returns a handler that renders the sign out confirm page. +func SignOutConfirm(data SignOutConfirmData) http.Handler { + return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return ui.ServePage(w, r, "SignOutConfirm", data.ToJSON()) + }) +} diff --git a/authenticate/handlers/userinfo.go b/authenticate/handlers/userinfo.go index 54ff9bd38..5ec61e6c4 100644 --- a/authenticate/handlers/userinfo.go +++ b/authenticate/handlers/userinfo.go @@ -20,7 +20,6 @@ type UserInfoData struct { DirectoryUser *directory.User IsImpersonated bool Session *session.Session - SignOutURL string User *user.User WebAuthnURL string } @@ -43,7 +42,6 @@ func (data UserInfoData) ToJSON() map[string]interface{} { 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) } diff --git a/authenticate/handlers/webauthn/webauthn.go b/authenticate/handlers/webauthn/webauthn.go index 8d6d73879..00655321e 100644 --- a/authenticate/handlers/webauthn/webauthn.go +++ b/authenticate/handlers/webauthn/webauthn.go @@ -13,7 +13,6 @@ import ( "net/url" "github.com/google/uuid" - "github.com/pomerium/webauthn" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" @@ -30,6 +29,7 @@ import ( "github.com/pomerium/pomerium/pkg/grpc/user" "github.com/pomerium/pomerium/pkg/webauthnutil" "github.com/pomerium/pomerium/ui" + "github.com/pomerium/webauthn" ) const maxAuthenticateResponses = 5 @@ -373,7 +373,6 @@ func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *Stat "creationOptions": creationOptions, "requestOptions": requestOptions, "selfUrl": r.URL.String(), - "signOutUrl": urlutil.SignOutURL(r, state.AuthenticateURL, state.SharedKey), }) } diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index b3312fc7e..4002e6bde 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -227,12 +227,84 @@ func TestAuthenticate_SignOut(t *testing.T) { wantCode int wantBody string }{ - {"good post", http.MethodPost, nil, "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""}, - {"signout redirect url", http.MethodPost, nil, "", "https://signout-redirect-url.example.com", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""}, - {"failed revoke", http.MethodPost, nil, "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""}, - {"load session error", http.MethodPost, errors.New("error"), "https://corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""}, - {"bad redirect uri", http.MethodPost, nil, "corp.pomerium.io/", "", "sig", "ts", identity.MockProvider{LogOutError: oidc.ErrSignoutNotImplemented}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusFound, ""}, - {"no redirect uri", http.MethodPost, nil, "", "", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{}}, http.StatusOK, "{\"Status\":200,\"Error\":\"OK: user logged out\"}\n"}, + { + "good post", + http.MethodPost, + nil, + "https://corp.pomerium.io/", + "", + "sig", + "ts", + identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, + &mstore.Store{Encrypted: true, Session: &sessions.State{}}, + http.StatusFound, + "", + }, + { + "signout redirect url", + http.MethodPost, + nil, + "", + "https://signout-redirect-url.example.com", + "sig", + "ts", + identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, + &mstore.Store{Encrypted: true, Session: &sessions.State{}}, + http.StatusFound, + "", + }, + { + "failed revoke", + http.MethodPost, + nil, + "https://corp.pomerium.io/", + "", + "sig", + "ts", + identity.MockProvider{RevokeError: errors.New("OH NO")}, + &mstore.Store{Encrypted: true, Session: &sessions.State{}}, + http.StatusFound, + "", + }, + { + "load session error", + http.MethodPost, + errors.New("error"), + "https://corp.pomerium.io/", + "", + "sig", + "ts", + identity.MockProvider{RevokeError: errors.New("OH NO")}, + &mstore.Store{Encrypted: true, Session: &sessions.State{}}, + http.StatusFound, + "", + }, + { + "bad redirect uri", + http.MethodPost, + nil, + "corp.pomerium.io/", + "", + "sig", + "ts", + identity.MockProvider{LogOutError: oidc.ErrSignoutNotImplemented}, + &mstore.Store{Encrypted: true, Session: &sessions.State{}}, + http.StatusFound, + "", + }, + { + "no redirect uri", + http.MethodPost, + nil, + "", + "", + "sig", + "ts", + identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, + &mstore.Store{Encrypted: true, Session: &sessions.State{}}, + http.StatusOK, + "{\"Status\":200,\"Error\":\"OK: user logged out\"}\n", + }, } for _, tt := range tests { tt := tt @@ -295,7 +367,7 @@ func TestAuthenticate_SignOut(t *testing.T) { r.Header.Set("Accept", "application/json") w := httptest.NewRecorder() - httputil.HandlerFunc(a.SignOut).ServeHTTP(w, r) + httputil.HandlerFunc(a.signOutRedirect).ServeHTTP(w, r) if status := w.Code; status != tt.wantCode { t.Errorf("handler returned wrong status code: got %v want %v", status, tt.wantCode) } @@ -727,89 +799,6 @@ func (m mockDirectoryServiceClient) RefreshUser(ctx context.Context, in *directo return nil, status.Error(codes.Unimplemented, "") } -func TestAuthenticate_SignOut_CSRF(t *testing.T) { - now := time.Now() - signer, err := jws.NewHS256Signer(nil) - if err != nil { - t.Fatal(err) - } - pbNow, _ := ptypes.TimestampProto(now) - a := &Authenticate{ - options: config.NewAtomicOptions(), - state: newAtomicAuthenticateState(&authenticateState{ - // sessionStore: tt.sessionStore, - cookieSecret: cryptutil.NewKey(), - encryptedEncoder: signer, - sharedEncoder: signer, - dataBrokerClient: mockDataBrokerServiceClient{ - get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) { - data, err := ptypes.MarshalAny(&session.Session{ - Id: "SESSION_ID", - UserId: "USER_ID", - IdToken: &session.IDToken{IssuedAt: pbNow}, - }) - if err != nil { - return nil, err - } - - return &databroker.GetResponse{ - Record: &databroker.Record{ - Version: 1, - Type: data.GetTypeUrl(), - Id: "SESSION_ID", - Data: data, - }, - }, nil - }, - }, - directoryClient: new(mockDirectoryServiceClient), - }), - } - tests := []struct { - name string - setCSRFCookie bool - method string - wantStatus int - wantBody string - }{ - {"GET without CSRF should fail", false, "GET", 400, "{\"Status\":400,\"Error\":\"Bad Request: CSRF token invalid\"}\n"}, - {"POST without CSRF should fail", false, "POST", 400, "{\"Status\":400,\"Error\":\"Bad Request: CSRF token invalid\"}\n"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := a.Handler() - - // Obtain a CSRF cookie via a GET request. - orr, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } - rr := httptest.NewRecorder() - s.ServeHTTP(rr, orr) - - r, err := http.NewRequest(tt.method, "/.pomerium/sign_out", nil) - if err != nil { - t.Fatal(err) - } - if tt.setCSRFCookie { - r.Header.Set("Cookie", rr.Header().Get("Set-Cookie")) - } - r.Header.Set("Accept", "application/json") - r.Header.Set("Referer", "/") - rr = httptest.NewRecorder() - s.ServeHTTP(rr, r) - - if rr.Code != tt.wantStatus { - t.Errorf("status: got %v want %v", rr.Code, tt.wantStatus) - } - body := rr.Body.String() - if diff := cmp.Diff(body, tt.wantBody); diff != "" { - t.Errorf("handler returned wrong body Body: %s", diff) - } - }) - } -} - func mustParseURL(rawurl string) *url.URL { u, err := url.Parse(rawurl) if err != nil { diff --git a/authenticate/state.go b/authenticate/state.go index dfbd02a65..c63f25096 100644 --- a/authenticate/state.go +++ b/authenticate/state.go @@ -9,7 +9,6 @@ import ( "sync/atomic" "github.com/go-jose/go-jose/v3" - "github.com/pomerium/webauthn" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/encoding" @@ -24,6 +23,7 @@ import ( "github.com/pomerium/pomerium/pkg/grpc/databroker" "github.com/pomerium/pomerium/pkg/grpc/directory" "github.com/pomerium/pomerium/pkg/webauthnutil" + "github.com/pomerium/webauthn" ) var outboundGRPCConnection = new(grpc.CachedOutboundGRPClientConn) diff --git a/internal/httputil/router.go b/internal/httputil/router.go index 1dec755fd..d4273c751 100644 --- a/internal/httputil/router.go +++ b/internal/httputil/router.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" - "github.com/pomerium/csrf" + "github.com/pomerium/csrf" "github.com/pomerium/pomerium/ui" ) diff --git a/pkg/webauthnutil/credential_storage.go b/pkg/webauthnutil/credential_storage.go index feb24dad7..84a520ba1 100644 --- a/pkg/webauthnutil/credential_storage.go +++ b/pkg/webauthnutil/credential_storage.go @@ -3,13 +3,13 @@ package webauthnutil import ( "context" - "github.com/pomerium/webauthn" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/pomerium/pomerium/pkg/encoding/base58" "github.com/pomerium/pomerium/pkg/grpc/databroker" "github.com/pomerium/pomerium/pkg/grpc/device" + "github.com/pomerium/webauthn" ) // CredentialStorage stores credentials in the databroker. diff --git a/pkg/webauthnutil/credential_storage_test.go b/pkg/webauthnutil/credential_storage_test.go index fd776694d..2d3c49df8 100644 --- a/pkg/webauthnutil/credential_storage_test.go +++ b/pkg/webauthnutil/credential_storage_test.go @@ -4,13 +4,13 @@ import ( "context" "testing" - "github.com/pomerium/webauthn" "github.com/stretchr/testify/assert" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/pomerium/pomerium/pkg/grpc/databroker" + "github.com/pomerium/webauthn" ) type mockDataBrokerServiceClient struct { diff --git a/pkg/webauthnutil/device_type.go b/pkg/webauthnutil/device_type.go index de66f9dd2..30db8a14a 100644 --- a/pkg/webauthnutil/device_type.go +++ b/pkg/webauthnutil/device_type.go @@ -3,7 +3,6 @@ package webauthnutil import ( "context" - "github.com/pomerium/webauthn/cose" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" @@ -11,6 +10,7 @@ import ( "github.com/pomerium/pomerium/internal/urlutil" "github.com/pomerium/pomerium/pkg/grpc/databroker" "github.com/pomerium/pomerium/pkg/grpc/device" + "github.com/pomerium/webauthn/cose" ) // DefaultDeviceType is the default device type when none is specified. diff --git a/pkg/webauthnutil/options_test.go b/pkg/webauthnutil/options_test.go index 91677c4a5..64276750f 100644 --- a/pkg/webauthnutil/options_test.go +++ b/pkg/webauthnutil/options_test.go @@ -3,12 +3,12 @@ package webauthnutil import ( "testing" - "github.com/pomerium/webauthn" - "github.com/pomerium/webauthn/cose" "github.com/stretchr/testify/assert" "github.com/pomerium/pomerium/pkg/grpc/device" "github.com/pomerium/pomerium/pkg/grpc/user" + "github.com/pomerium/webauthn" + "github.com/pomerium/webauthn/cose" ) func TestGenerateCreationOptions(t *testing.T) { diff --git a/pkg/webauthnutil/user.go b/pkg/webauthnutil/user.go index d5e86178c..2c3fe06f6 100644 --- a/pkg/webauthnutil/user.go +++ b/pkg/webauthnutil/user.go @@ -2,9 +2,9 @@ package webauthnutil import ( "github.com/google/uuid" - "github.com/pomerium/webauthn" "github.com/pomerium/pomerium/pkg/grpc/user" + "github.com/pomerium/webauthn" ) var pomeriumUserNamespace = uuid.MustParse("2929d3f7-f0b0-478f-9dd5-970d51eb3859") diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5cce3b31d..edd84f9e8 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -2,16 +2,17 @@ import DeviceEnrolledPage from "./components/DeviceEnrolledPage"; import ErrorPage from "./components/ErrorPage"; import Footer from "./components/Footer"; import Header from "./components/Header"; +import SignOutConfirmPage from "./components/SignOutConfirmPage"; +import { ToolbarOffset } from "./components/ToolbarOffset"; import UserInfoPage from "./components/UserInfoPage"; import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage"; +import { SubpageContextProvider } from "./context/Subpage"; import { createTheme } from "./theme"; import { PageData } from "./types"; +import Box from "@mui/material/Box"; import CssBaseline from "@mui/material/CssBaseline"; import { ThemeProvider } from "@mui/material/styles"; import React, { FC } from "react"; -import {ToolbarOffset} from "./components/ToolbarOffset"; -import Box from "@mui/material/Box"; -import {SubpageContextProvider} from "./context/Subpage"; const theme = createTheme(); @@ -25,6 +26,9 @@ const App: FC = () => { case "Error": body = ; break; + case "SignOutConfirm": + body = ; + break; case "UserInfo": body = ; break; @@ -36,15 +40,16 @@ const App: FC = () => { -
+
- - + + {body} diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index 9dd2c2510..05f257bdf 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -1,23 +1,29 @@ +import { PageData } from "../types"; +import { Avatar } from "./Avatar"; import Logo from "./Logo"; +import { ToolbarOffset } from "./ToolbarOffset"; +import UserSidebarContent from "./UserSidebarContent"; +import { + Drawer, + IconButton, + Menu, + MenuItem, + useMediaQuery, +} from "@mui/material"; import AppBar from "@mui/material/AppBar"; import Box from "@mui/material/Box"; import Toolbar from "@mui/material/Toolbar"; -import React, {FC, useState} from "react"; -import {useTheme} from "@mui/material/styles"; -import {Drawer, IconButton, Menu, MenuItem, useMediaQuery} from "@mui/material"; -import {ToolbarOffset} from "./ToolbarOffset"; -import UserSidebarContent from "./UserSidebarContent"; -import {ChevronLeft, ChevronRight, Menu as MenuIcon} from "react-feather"; +import { useTheme } from "@mui/material/styles"; import styled from "@mui/material/styles/styled"; -import {Avatar} from "./Avatar"; -import {PageData} from "../types"; -import {get} from 'lodash'; +import { get } from "lodash"; +import React, { FC, useState } from "react"; +import { ChevronLeft, ChevronRight, Menu as MenuIcon } from "react-feather"; -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', +const DrawerHeader = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", padding: theme.spacing(0, 1), - justifyContent: 'flex-end', + justifyContent: "flex-end", })); type HeaderProps = { @@ -26,33 +32,34 @@ type HeaderProps = { }; const Header: FC = ({ includeSidebar, data }) => { const theme = useTheme(); - const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), { + const mdUp = useMediaQuery(() => theme.breakpoints.up("md"), { defaultMatches: true, - noSsr: false + noSsr: false, }); const [drawerOpen, setDrawerOpen] = useState(false); const [anchorEl, setAnchorEl] = React.useState(null); - const handleMenuOpen = e => { + const handleMenuOpen = (e) => { setAnchorEl(e.currentTarget); }; const handleMenuClose = () => { setAnchorEl(null); }; - const userName = get(data, 'user.name') || get(data, 'user.claims.given_name'); + const userName = + get(data, "user.name") || get(data, "user.claims.given_name"); const handleDrawerOpen = () => { setDrawerOpen(true); }; - const handleDrawerClose = ():void => { + const handleDrawerClose = (): void => { setDrawerOpen(false); }; - const handleLogout = (evt: React.MouseEvent):void => { + const handleLogout = (evt: React.MouseEvent): void => { evt.preventDefault(); location.href = "/.pomerium/sign_out"; - } + }; return ( = ({ includeSidebar, data }) => { aria-label="open drawer" onClick={handleDrawerOpen} edge="start" - sx={{ mr: 2, ...(drawerOpen && { display: 'none' }) }} + sx={{ mr: 2, ...(drawerOpen && { display: "none" }) }} > @@ -75,11 +82,11 @@ const Header: FC = ({ includeSidebar, data }) => { sx={{ width: 256, flexShrink: 0, - '& .MuiDrawer-paper': { + "& .MuiDrawer-paper": { width: 256, - boxSizing: 'border-box', - backgroundColor: 'neutral.900', - height: '100vh', + boxSizing: "border-box", + backgroundColor: "neutral.900", + height: "100vh", }, }} variant="persistent" @@ -88,10 +95,14 @@ const Header: FC = ({ includeSidebar, data }) => { > - {theme.direction === 'ltr' ? : } + {theme.direction === "ltr" ? ( + + ) : ( + + )} - + @@ -104,13 +115,16 @@ const Header: FC = ({ includeSidebar, data }) => { {userName && ( <> - + = ({ data }) => { + function handleClickCancel(evt: React.MouseEvent) { + evt.preventDefault(); + history.back(); + } + + function handleClickLogout(evt: React.MouseEvent) { + evt.preventDefault(); + location.href = data.url; + } + + return ( + + + Logout? + + + Are you sure you want to logout? + + + + + + + + + ); +}; +export default SignOutConfirmPage; diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 2e5d5d899..96cd96642 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -81,7 +81,6 @@ export type WebAuthnRequestOptions = { type BasePageData = { csrfToken?: string; - signOutUrl?: string; }; export type ErrorPageData = BasePageData & { @@ -100,6 +99,11 @@ export type DeviceEnrolledPageData = BasePageData & { page: "DeviceEnrolled"; }; +export type SignOutConfirmPageData = BasePageData & { + page: "SignOutConfirm"; + url: string; +}; + export type UserInfoPageData = BasePageData & { page: "UserInfo"; @@ -123,5 +127,6 @@ export type WebAuthnRegistrationPageData = BasePageData & { export type PageData = | ErrorPageData | DeviceEnrolledPageData + | SignOutConfirmPageData | UserInfoPageData | WebAuthnRegistrationPageData;