4 changes: 2 additions & 2 deletions console/ui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
<meta charset="utf-8">
<title>Nakama Console</title>
<base href="/">
<style type="text/css">@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_ZpC3gTD_vx3rCubqg.woff2) format('woff2');unicode-range:U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_ZpC3g3D_vx3rCubqg.woff2) format('woff2');unicode-range:U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_ZpC3gbD_vx3rCubqg.woff2) format('woff2');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_ZpC3gfD_vx3rCubqg.woff2) format('woff2');unicode-range:U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v18/JTURjIg1_i6t8kCHKm45_ZpC3gnD_vx3rCs.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0370-03FF;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v27/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVQUwaEQbjA.woff) format('woff');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}</style>
<style type="text/css">@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v24/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtZ6Hw0aXx-p7K4KLjztg.woff) format('woff');unicode-range:U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v24/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtZ6Hw9aXx-p7K4KLjztg.woff) format('woff');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v24/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtZ6Hw2aXx-p7K4KLjztg.woff) format('woff');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v24/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtZ6Hw3aXx-p7K4KLjztg.woff) format('woff');unicode-range:U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Montserrat';font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/montserrat/v24/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtZ6Hw5aXx-p7K4KLg.woff) format('woff');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0370-03FF;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVQUwaEQbjB_mQ.woff) format('woff');unicode-range:U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/opensans/v29/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVQUwaEQbjA.woff) format('woff');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}</style>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="static/styles.14b882f135e080634619.css"></head>
<body class="h-100">
<app-root></app-root>
<script src="static/runtime.4ced225923cd14368d64.js" defer=""></script><script src="static/polyfills.cb4331e883de4daa4c94.js" defer=""></script><script src="static/main.0b74ccf8d7e0caf7719b.js" defer=""></script></body>
<script src="static/runtime.4ced225923cd14368d64.js" defer=""></script><script src="static/polyfills.cb4331e883de4daa4c94.js" defer=""></script><script src="static/main.2778305943c5228ee227.js" defer=""></script></body>
</html>

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions console/ui/src/app/authentication-error.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export class AuthenticationErrorInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(catchError(err => {
if (err.status === 401) {
this.authenticationService.logout();

if (!req.url.includes('/v3/auth')) {
// only reload the page if we aren't on the auth pages, this is so that we can display the auth errors.
const stateUrl = this.router.routerState.snapshot.url;
const _ = this.router.navigate(['/login'], {queryParams: {next: stateUrl}});
}
this.authenticationService.logout().subscribe({
next: () => {
if (!req.url.includes('/v3/auth')) {
// only reload the page if we aren't on the auth pages, this is so that we can display the auth errors.
const stateUrl = this.router.routerState.snapshot.url;
const _ = this.router.navigate(['/login'], {queryParams: {next: stateUrl}});
}
}
});
} else if (err.status >= 500) {
console.log(`${err.status}: + ${err.error.message || err.statusText}`);
}
Expand Down
16 changes: 11 additions & 5 deletions console/ui/src/app/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import {Inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {BehaviorSubject, Observable} from 'rxjs';
import {BehaviorSubject, EMPTY, Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {ConsoleService, ConsoleSession, UserRole} from './console.service';
import {WINDOW} from './window.provider';
Expand Down Expand Up @@ -79,10 +79,16 @@ export class AuthenticationService {
}));
}

logout(): void {
localStorage.removeItem(SESSION_LOCALSTORAGE_KEY);
// @ts-ignore
this.currentSessionSubject.next(null);
logout(): Observable<any> {
if (!this.currentSessionSubject.getValue()) {
return EMPTY;
}
return this.consoleService.authenticateLogout('', {
token: this.currentSessionSubject.getValue()?.token,
}).pipe(tap(() => {
localStorage.removeItem(SESSION_LOCALSTORAGE_KEY);
this.currentSessionSubject.next(null);
}));
}

segmentIdentify(session): void {
Expand Down
4 changes: 3 additions & 1 deletion console/ui/src/app/base/base.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ export class BaseComponent implements OnInit, OnDestroy {
}

logout(): void {
this.authService.logout();
this.authService.logout().subscribe(() => {
this.router.navigate(['/login']);
});
}

ngOnDestroy(): void {
Expand Down
13 changes: 13 additions & 0 deletions console/ui/src/app/console.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export interface ApiEndpointList {
rpc_endpoints?:Array<ApiEndpointDescriptor>
}

/** Log out a session and invalidate a session token. */
export interface AuthenticateLogoutRequest {
// Session token to log out.
token?:string
}

/** Authenticate a console user with username and password. */
export interface AuthenticateRequest {
// The password of the user.
Expand Down Expand Up @@ -1013,6 +1019,13 @@ export class ConsoleService {
return this.httpClient.post<ConsoleSession>(this.config.host + urlPath, body, { params: params })
}

/** Log out a session and invalidate the session token. */
authenticateLogout(auth_token: string, body: AuthenticateLogoutRequest): Observable<any> {
const urlPath = `/v2/console/authenticate/logout`;
let params = new HttpParams();
return this.httpClient.post(this.config.host + urlPath, body, { params: params, headers: this.getTokenAuthHeaders(auth_token) })
}

/** Get server config and configuration warnings. */
getConfig(auth_token: string): Observable<Config> {
const urlPath = `/v2/console/config`;
Expand Down
5 changes: 3 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ func main() {
cookie := newOrLoadCookie(config)
metrics := server.NewLocalMetrics(logger, startupLogger, db, config)
sessionRegistry := server.NewLocalSessionRegistry(metrics)
sessionCache := server.NewLocalSessionCache(config)
sessionCache := server.NewLocalSessionCache(config, config.GetSession().TokenExpirySec)
consoleSessionCache := server.NewLocalSessionCache(config, config.GetConsole().TokenExpirySec)
statusRegistry := server.NewStatusRegistry(logger, config, sessionRegistry, jsonpbMarshaler)
tracker := server.StartLocalTracker(logger, config, sessionRegistry, statusRegistry, metrics, jsonpbMarshaler)
router := server.NewLocalMessageRouter(sessionRegistry, tracker, jsonpbMarshaler)
Expand All @@ -165,7 +166,7 @@ func main() {
statusHandler := server.NewLocalStatusHandler(logger, sessionRegistry, matchRegistry, tracker, metrics, config.GetName())

apiServer := server.StartApiServer(logger, startupLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, leaderboardCache, leaderboardRankCache, sessionRegistry, sessionCache, statusRegistry, matchRegistry, matchmaker, tracker, router, streamManager, metrics, pipeline, runtime)
consoleServer := server.StartConsoleServer(logger, startupLogger, db, config, tracker, router, streamManager, sessionCache, statusRegistry, statusHandler, runtimeInfo, matchRegistry, configWarnings, semver, leaderboardCache, leaderboardRankCache, apiServer, cookie)
consoleServer := server.StartConsoleServer(logger, startupLogger, db, config, tracker, router, streamManager, sessionCache, consoleSessionCache, statusRegistry, statusHandler, runtimeInfo, matchRegistry, configWarnings, semver, leaderboardCache, leaderboardRankCache, apiServer, cookie)

gaenabled := len(os.Getenv("NAKAMA_TELEMETRY")) < 1
const gacode = "UA-89792135-1"
Expand Down
29 changes: 22 additions & 7 deletions server/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"crypto"
"database/sql"
"fmt"
"github.com/gofrs/uuid"
"io/ioutil"
"math"
"net"
Expand Down Expand Up @@ -136,6 +137,7 @@ type ConsoleServer struct {
router MessageRouter
StreamManager StreamManager
sessionCache SessionCache
consoleSessionCache SessionCache
statusRegistry *StatusRegistry
matchRegistry MatchRegistry
statusHandler StatusHandler
Expand All @@ -153,7 +155,7 @@ type ConsoleServer struct {
httpClient *http.Client
}

func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, config Config, tracker Tracker, router MessageRouter, streamManager StreamManager, sessionCache SessionCache, statusRegistry *StatusRegistry, statusHandler StatusHandler, runtimeInfo *RuntimeInfo, matchRegistry MatchRegistry, configWarnings map[string]string, serverVersion string, leaderboardCache LeaderboardCache, leaderboardRankCache LeaderboardRankCache, api *ApiServer, cookie string) *ConsoleServer {
func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.DB, config Config, tracker Tracker, router MessageRouter, streamManager StreamManager, sessionCache SessionCache, consoleSessionCache SessionCache, statusRegistry *StatusRegistry, statusHandler StatusHandler, runtimeInfo *RuntimeInfo, matchRegistry MatchRegistry, configWarnings map[string]string, serverVersion string, leaderboardCache LeaderboardCache, leaderboardRankCache LeaderboardRankCache, api *ApiServer, cookie string) *ConsoleServer {
var gatewayContextTimeoutMs string
if config.GetConsole().IdleTimeoutMs > 500 {
// Ensure the GRPC Gateway timeout is just under the idle timeout (if possible) to ensure it has priority.
Expand All @@ -165,7 +167,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.D
serverOpts := []grpc.ServerOption{
//grpc.StatsHandler(&ocgrpc.ServerHandler{IsPublicEndpoint: true}),
grpc.MaxRecvMsgSize(int(config.GetConsole().MaxMessageSizeBytes)),
grpc.UnaryInterceptor(consoleInterceptorFunc(logger, config)),
grpc.UnaryInterceptor(consoleInterceptorFunc(logger, config, consoleSessionCache)),
}
grpcServer := grpc.NewServer(serverOpts...)

Expand All @@ -179,6 +181,7 @@ func StartConsoleServer(logger *zap.Logger, startupLogger *zap.Logger, db *sql.D
router: router,
StreamManager: streamManager,
sessionCache: sessionCache,
consoleSessionCache: consoleSessionCache,
statusRegistry: statusRegistry,
matchRegistry: matchRegistry,
statusHandler: statusHandler,
Expand Down Expand Up @@ -423,12 +426,15 @@ func (s *ConsoleServer) Stop() {
s.grpcServer.GracefulStop()
}

func consoleInterceptorFunc(logger *zap.Logger, config Config) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) {
func consoleInterceptorFunc(logger *zap.Logger, config Config, sessionCache SessionCache) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if info.FullMethod == "/nakama.console.Console/Authenticate" {
// Skip authentication check for Login endpoint.
return handler(ctx, req)
}
if info.FullMethod == "/nakama.console.Console/AuthenticateLogout" {
return handler(ctx, req)
}

md, ok := metadata.FromIncomingContext(ctx)
if !ok {
Expand All @@ -446,7 +452,7 @@ func consoleInterceptorFunc(logger *zap.Logger, config Config) func(context.Cont
return nil, status.Error(codes.Unauthenticated, "Console authentication required.")
}

if ctx, ok = checkAuth(ctx, config, auth[0]); !ok {
if ctx, ok = checkAuth(ctx, config, auth[0], sessionCache); !ok {
return nil, status.Error(codes.Unauthenticated, "Console authentication invalid.")
}
role := ctx.Value(ctxConsoleRoleKey{}).(console.UserRole)
Expand All @@ -460,7 +466,7 @@ func consoleInterceptorFunc(logger *zap.Logger, config Config) func(context.Cont
}
}

func checkAuth(ctx context.Context, config Config, auth string) (context.Context, bool) {
func checkAuth(ctx context.Context, config Config, auth string, sessionCache SessionCache) (context.Context, bool) {
const basicPrefix = "Basic "
const bearerPrefix = "Bearer "

Expand All @@ -481,7 +487,8 @@ func checkAuth(ctx context.Context, config Config, auth string) (context.Context
return ctx, true
} else if strings.HasPrefix(auth, bearerPrefix) {
// Bearer token authentication.
token, err := jwt.Parse(auth[len(bearerPrefix):], func(token *jwt.Token) (interface{}, error) {
tokenStr := auth[len(bearerPrefix):]
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
if s, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || s.Hash != crypto.SHA256 {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
Expand All @@ -491,7 +498,7 @@ func checkAuth(ctx context.Context, config Config, auth string) (context.Context
// Token verification failed.
return ctx, false
}
uname, email, role, exp, ok := parseConsoleToken([]byte(config.GetConsole().SigningKey), auth[len(bearerPrefix):])
id, uname, email, role, exp, ok := parseConsoleToken([]byte(config.GetConsole().SigningKey), tokenStr)
if !ok || !token.Valid {
// The token or its claims are invalid.
return ctx, false
Expand All @@ -504,6 +511,14 @@ func checkAuth(ctx context.Context, config Config, auth string) (context.Context
// Token expired.
return ctx, false
}
userId, err := uuid.FromString(id)
if err != nil {
// Malformed id
return ctx, false
}
if !sessionCache.IsValidSession(userId, exp, tokenStr) {
return ctx, false
}

ctx = context.WithValue(context.WithValue(context.WithValue(ctx, ctxConsoleRoleKey{}, role), ctxConsoleUsernameKey{}, uname), ctxConsoleEmailKey{}, email)

Expand Down
45 changes: 38 additions & 7 deletions server/console_authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/gofrs/uuid"
"google.golang.org/protobuf/types/known/emptypb"
"time"

jwt "github.com/golang-jwt/jwt/v4"
Expand All @@ -32,6 +34,7 @@ import (
)

type ConsoleTokenClaims struct {
ID string `json:"id,omitempty"`
Username string `json:"usn,omitempty"`
Email string `json:"ema,omitempty"`
Role console.UserRole `json:"rol,omitempty"`
Expand All @@ -50,7 +53,7 @@ func (stc *ConsoleTokenClaims) Valid() error {
return nil
}

func parseConsoleToken(hmacSecretByte []byte, tokenString string) (username, email string, role console.UserRole, exp int64, ok bool) {
func parseConsoleToken(hmacSecretByte []byte, tokenString string) (id, username, email string, role console.UserRole, exp int64, ok bool) {
token, err := jwt.ParseWithClaims(tokenString, &ConsoleTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if s, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || s.Hash != crypto.SHA256 {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
Expand All @@ -64,22 +67,24 @@ func parseConsoleToken(hmacSecretByte []byte, tokenString string) (username, ema
if !ok || !token.Valid {
return
}
return claims.Username, claims.Email, claims.Role, claims.ExpiresAt, true
return claims.ID, claims.Username, claims.Email, claims.Role, claims.ExpiresAt, true
}

func (s *ConsoleServer) Authenticate(ctx context.Context, in *console.AuthenticateRequest) (*console.ConsoleSession, error) {
role := console.UserRole_USER_ROLE_UNKNOWN
var uname string
var email string
var id uuid.UUID
switch in.Username {
case s.config.GetConsole().Username:
if in.Password == s.config.GetConsole().Password {
role = console.UserRole_USER_ROLE_ADMIN
uname = in.Username
id = uuid.Nil
}
default:
var err error
uname, email, role, err = s.lookupConsoleUser(ctx, in.Username, in.Password)
id, uname, email, role, err = s.lookupConsoleUser(ctx, in.Username, in.Password)
if err != nil {
return nil, err
}
Expand All @@ -89,24 +94,50 @@ func (s *ConsoleServer) Authenticate(ctx context.Context, in *console.Authentica
return nil, status.Error(codes.Unauthenticated, "Invalid credentials.")
}

exp := time.Now().UTC().Add(time.Duration(s.config.GetConsole().TokenExpirySec) * time.Second).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &ConsoleTokenClaims{
ExpiresAt: time.Now().UTC().Add(time.Duration(s.config.GetConsole().TokenExpirySec) * time.Second).Unix(),
ExpiresAt: exp,
ID: id.String(),
Username: uname,
Email: email,
Role: role,
Cookie: s.cookie,
})
key := []byte(s.config.GetConsole().SigningKey)
signedToken, _ := token.SignedString(key)

s.consoleSessionCache.Add(id, exp, signedToken, 0, "")
return &console.ConsoleSession{Token: signedToken}, nil
}

func (s *ConsoleServer) lookupConsoleUser(ctx context.Context, unameOrEmail, password string) (uname string, email string, role console.UserRole, err error) {
func (s *ConsoleServer) AuthenticateLogout(ctx context.Context, in *console.AuthenticateLogoutRequest) (*emptypb.Empty, error) {
token, err := jwt.Parse(in.Token, func(token *jwt.Token) (interface{}, error) {
if s, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || s.Hash != crypto.SHA256 {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.config.GetConsole().SigningKey), nil
})
if err != nil {
s.logger.Error("Failed to parse the session token.", zap.Error(err))
}
id, _, _, _, exp, ok := parseConsoleToken([]byte(s.config.GetConsole().SigningKey), in.Token)
if !ok || !token.Valid {
s.logger.Error("Invalid token.", zap.Error(err))
}
idUuid, err := uuid.FromString(id)
if id != "" && err == nil {
s.consoleSessionCache.Remove(idUuid, exp, in.Token, 0, "")
}

return &emptypb.Empty{}, nil
}

func (s *ConsoleServer) lookupConsoleUser(ctx context.Context, unameOrEmail, password string) (id uuid.UUID, uname string, email string, role console.UserRole, err error) {
role = console.UserRole_USER_ROLE_UNKNOWN
query := "SELECT username, email, role, password, disable_time FROM console_user WHERE username = $1 OR email = $1"
query := "SELECT id, username, email, role, password, disable_time FROM console_user WHERE username = $1 OR email = $1"
var dbPassword []byte
var dbDisableTime pgtype.Timestamptz
err = s.db.QueryRowContext(ctx, query, unameOrEmail).Scan(&uname, &email, &role, &dbPassword, &dbDisableTime)
err = s.db.QueryRowContext(ctx, query, unameOrEmail).Scan(&id, &uname, &email, &role, &dbPassword, &dbDisableTime)
if err != nil {
if err == sql.ErrNoRows {
err = nil
Expand Down
2 changes: 1 addition & 1 deletion server/console_storage_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (s *ConsoleServer) importStorage(w http.ResponseWriter, r *http.Request) {
}
return
}
ctx, ok := checkAuth(r.Context(), s.config, auth)
ctx, ok := checkAuth(r.Context(), s.config, auth, s.consoleSessionCache)
if !ok {
w.WriteHeader(401)
if _, err := w.Write([]byte("Console authentication invalid.")); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions server/session_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type LocalSessionCache struct {
cache map[uuid.UUID]*sessionCacheUser
}

func NewLocalSessionCache(config Config) SessionCache {
func NewLocalSessionCache(config Config, tokenExpirySec int64) SessionCache {
ctx, ctxCancelFn := context.WithCancel(context.Background())

s := &LocalSessionCache{
Expand All @@ -69,7 +69,7 @@ func NewLocalSessionCache(config Config) SessionCache {
}

go func() {
ticker := time.NewTicker(2 * time.Duration(config.GetSession().TokenExpirySec) * time.Second)
ticker := time.NewTicker(2 * time.Duration(tokenExpirySec) * time.Second)
for {
select {
case <-s.ctx.Done():
Expand Down