VYPR
High severityNVD Advisory· Published Jul 5, 2022· Updated Aug 3, 2024

Insufficient Session Expiration in heroiclabs/nakama

CVE-2022-2306

Description

Old session tokens remain valid in Nakama, allowing attackers to authenticate and send requests; fixed by adding a logout endpoint.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Old session tokens remain valid in Nakama, allowing attackers to authenticate and send requests; fixed by adding a logout endpoint.

Vulnerability

CVE-2022-2306 describes a session token reuse vulnerability in the Nakama game server. The application fails to invalidate old session tokens, meaning tokens that have been used previously remain valid indefinitely. This allows an attacker who possesses an old token to authenticate to the server and perform authorized actions as if the token were still active [1].

Exploitation

An attacker can exploit this flaw by using an old session token to authenticate to the Nakama console API. No additional authentication is required if the token is already valid. The attacker can then send authenticated requests to the console, potentially accessing management functions or data [2].

Impact

Successful exploitation grants an attacker unauthorized access to the Nakama console, which could lead to exposure of sensitive data, manipulation of server configuration, or disruption of game services. The severity of the impact depends on the privileges associated with the compromised token [3].

Mitigation

Heroic Labs addressed this vulnerability in commit ce8d3921e2acd44ef8b5e6edfe595b6df067b166, which adds a /v2/console/authenticate/logout endpoint that invalidates session tokens upon logout [2]. The fix ensures that after a user logs out, the token can no longer be used for authentication. Users should update to a version containing this commit to protect against token reuse [3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/heroiclabs/nakamaGo
<= 3.12.0

Affected products

2

Patches

1
ce8d3921e2ac

Correctly console session token on all logouts. (#875)

https://github.com/heroiclabs/nakamaFernando TakagiJul 2, 2022via ghsa
16 files changed · +1528 1207
  • console/console_grpc.pb.go+38 0 modified
    @@ -22,6 +22,8 @@ const _ = grpc.SupportPackageIsVersion7
     type ConsoleClient interface {
     	// Authenticate a console user with username and password.
     	Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*ConsoleSession, error)
    +	// Log out a session and invalidate the session token.
    +	AuthenticateLogout(ctx context.Context, in *AuthenticateLogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
     	// Add a new console user.
     	AddUser(ctx context.Context, in *AddUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
     	// Ban a user.
    @@ -153,6 +155,15 @@ func (c *consoleClient) Authenticate(ctx context.Context, in *AuthenticateReques
     	return out, nil
     }
     
    +func (c *consoleClient) AuthenticateLogout(ctx context.Context, in *AuthenticateLogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
    +	out := new(emptypb.Empty)
    +	err := c.cc.Invoke(ctx, "/nakama.console.Console/AuthenticateLogout", in, out, opts...)
    +	if err != nil {
    +		return nil, err
    +	}
    +	return out, nil
    +}
    +
     func (c *consoleClient) AddUser(ctx context.Context, in *AddUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
     	out := new(emptypb.Empty)
     	err := c.cc.Invoke(ctx, "/nakama.console.Console/AddUser", in, out, opts...)
    @@ -663,6 +674,8 @@ func (c *consoleClient) WriteStorageObject(ctx context.Context, in *WriteStorage
     type ConsoleServer interface {
     	// Authenticate a console user with username and password.
     	Authenticate(context.Context, *AuthenticateRequest) (*ConsoleSession, error)
    +	// Log out a session and invalidate the session token.
    +	AuthenticateLogout(context.Context, *AuthenticateLogoutRequest) (*emptypb.Empty, error)
     	// Add a new console user.
     	AddUser(context.Context, *AddUserRequest) (*emptypb.Empty, error)
     	// Ban a user.
    @@ -785,6 +798,9 @@ type UnimplementedConsoleServer struct {
     func (UnimplementedConsoleServer) Authenticate(context.Context, *AuthenticateRequest) (*ConsoleSession, error) {
     	return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
     }
    +func (UnimplementedConsoleServer) AuthenticateLogout(context.Context, *AuthenticateLogoutRequest) (*emptypb.Empty, error) {
    +	return nil, status.Errorf(codes.Unimplemented, "method AuthenticateLogout not implemented")
    +}
     func (UnimplementedConsoleServer) AddUser(context.Context, *AddUserRequest) (*emptypb.Empty, error) {
     	return nil, status.Errorf(codes.Unimplemented, "method AddUser not implemented")
     }
    @@ -984,6 +1000,24 @@ func _Console_Authenticate_Handler(srv interface{}, ctx context.Context, dec fun
     	return interceptor(ctx, in, info, handler)
     }
     
    +func _Console_AuthenticateLogout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
    +	in := new(AuthenticateLogoutRequest)
    +	if err := dec(in); err != nil {
    +		return nil, err
    +	}
    +	if interceptor == nil {
    +		return srv.(ConsoleServer).AuthenticateLogout(ctx, in)
    +	}
    +	info := &grpc.UnaryServerInfo{
    +		Server:     srv,
    +		FullMethod: "/nakama.console.Console/AuthenticateLogout",
    +	}
    +	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
    +		return srv.(ConsoleServer).AuthenticateLogout(ctx, req.(*AuthenticateLogoutRequest))
    +	}
    +	return interceptor(ctx, in, info, handler)
    +}
    +
     func _Console_AddUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
     	in := new(AddUserRequest)
     	if err := dec(in); err != nil {
    @@ -2003,6 +2037,10 @@ var Console_ServiceDesc = grpc.ServiceDesc{
     			MethodName: "Authenticate",
     			Handler:    _Console_Authenticate_Handler,
     		},
    +		{
    +			MethodName: "AuthenticateLogout",
    +			Handler:    _Console_AuthenticateLogout_Handler,
    +		},
     		{
     			MethodName: "AddUser",
     			Handler:    _Console_AddUser_Handler,
    
  • console/console.pb.go+1247 1172 modified
  • console/console.pb.gw.go+81 0 modified
    @@ -67,6 +67,40 @@ func local_request_Console_Authenticate_0(ctx context.Context, marshaler runtime
     
     }
     
    +func request_Console_AuthenticateLogout_0(ctx context.Context, marshaler runtime.Marshaler, client ConsoleClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
    +	var protoReq AuthenticateLogoutRequest
    +	var metadata runtime.ServerMetadata
    +
    +	newReader, berr := utilities.IOReaderFactory(req.Body)
    +	if berr != nil {
    +		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
    +	}
    +	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
    +		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
    +	}
    +
    +	msg, err := client.AuthenticateLogout(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
    +	return msg, metadata, err
    +
    +}
    +
    +func local_request_Console_AuthenticateLogout_0(ctx context.Context, marshaler runtime.Marshaler, server ConsoleServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
    +	var protoReq AuthenticateLogoutRequest
    +	var metadata runtime.ServerMetadata
    +
    +	newReader, berr := utilities.IOReaderFactory(req.Body)
    +	if berr != nil {
    +		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
    +	}
    +	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
    +		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
    +	}
    +
    +	msg, err := server.AuthenticateLogout(ctx, &protoReq)
    +	return msg, metadata, err
    +
    +}
    +
     func request_Console_AddUser_0(ctx context.Context, marshaler runtime.Marshaler, client ConsoleClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
     	var protoReq AddUserRequest
     	var metadata runtime.ServerMetadata
    @@ -3058,6 +3092,29 @@ func RegisterConsoleHandlerServer(ctx context.Context, mux *runtime.ServeMux, se
     
     	})
     
    +	mux.Handle("POST", pattern_Console_AuthenticateLogout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
    +		ctx, cancel := context.WithCancel(req.Context())
    +		defer cancel()
    +		var stream runtime.ServerTransportStream
    +		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
    +		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
    +		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/nakama.console.Console/AuthenticateLogout", runtime.WithHTTPPathPattern("/v2/console/authenticate/logout"))
    +		if err != nil {
    +			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
    +			return
    +		}
    +		resp, md, err := local_request_Console_AuthenticateLogout_0(rctx, inboundMarshaler, server, req, pathParams)
    +		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
    +		ctx = runtime.NewServerMetadataContext(ctx, md)
    +		if err != nil {
    +			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
    +			return
    +		}
    +
    +		forward_Console_AuthenticateLogout_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
    +
    +	})
    +
     	mux.Handle("POST", pattern_Console_AddUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
     		ctx, cancel := context.WithCancel(req.Context())
     		defer cancel()
    @@ -4430,6 +4487,26 @@ func RegisterConsoleHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl
     
     	})
     
    +	mux.Handle("POST", pattern_Console_AuthenticateLogout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
    +		ctx, cancel := context.WithCancel(req.Context())
    +		defer cancel()
    +		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
    +		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/nakama.console.Console/AuthenticateLogout", runtime.WithHTTPPathPattern("/v2/console/authenticate/logout"))
    +		if err != nil {
    +			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
    +			return
    +		}
    +		resp, md, err := request_Console_AuthenticateLogout_0(rctx, inboundMarshaler, client, req, pathParams)
    +		ctx = runtime.NewServerMetadataContext(ctx, md)
    +		if err != nil {
    +			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
    +			return
    +		}
    +
    +		forward_Console_AuthenticateLogout_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
    +
    +	})
    +
     	mux.Handle("POST", pattern_Console_AddUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
     		ctx, cancel := context.WithCancel(req.Context())
     		defer cancel()
    @@ -5576,6 +5653,8 @@ func RegisterConsoleHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl
     var (
     	pattern_Console_Authenticate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "console", "authenticate"}, ""))
     
    +	pattern_Console_AuthenticateLogout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "console", "authenticate", "logout"}, ""))
    +
     	pattern_Console_AddUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "console", "user"}, ""))
     
     	pattern_Console_BanAccount_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"v2", "console", "account", "id", "ban"}, ""))
    @@ -5694,6 +5773,8 @@ var (
     var (
     	forward_Console_Authenticate_0 = runtime.ForwardResponseMessage
     
    +	forward_Console_AuthenticateLogout_0 = runtime.ForwardResponseMessage
    +
     	forward_Console_AddUser_0 = runtime.ForwardResponseMessage
     
     	forward_Console_BanAccount_0 = runtime.ForwardResponseMessage
    
  • console/console.proto+14 0 modified
    @@ -86,6 +86,14 @@ service Console {
         };
       }
     
    +  // Log out a session and invalidate the session token.
    +  rpc AuthenticateLogout (AuthenticateLogoutRequest) returns (google.protobuf.Empty) {
    +    option (google.api.http) = {
    +      post: "/v2/console/authenticate/logout",
    +      body: "*"
    +    };
    +  }
    +
       // Add a new console user.
       rpc AddUser (AddUserRequest) returns (google.protobuf.Empty) {
         option (google.api.http) = {
    @@ -504,6 +512,12 @@ message AuthenticateRequest {
       string password = 2;
     }
     
    +// Log out a session and invalidate a session token.
    +message AuthenticateLogoutRequest {
    +  // Session token to log out.
    +  string token = 1;
    +}
    +
     // API Explorer request definition for CallApiEndpoint
     message CallApiEndpointRequest {
       string method = 1;
    
  • console/console.swagger.json+43 0 modified
    @@ -1060,6 +1060,39 @@
             ]
           }
         },
    +    "/v2/console/authenticate/logout": {
    +      "post": {
    +        "summary": "Log out a session and invalidate the session token.",
    +        "operationId": "Console_AuthenticateLogout",
    +        "responses": {
    +          "200": {
    +            "description": "A successful response.",
    +            "schema": {
    +              "properties": {}
    +            }
    +          },
    +          "default": {
    +            "description": "An unexpected error response.",
    +            "schema": {
    +              "$ref": "#/definitions/googlerpcStatus"
    +            }
    +          }
    +        },
    +        "parameters": [
    +          {
    +            "name": "body",
    +            "in": "body",
    +            "required": true,
    +            "schema": {
    +              "$ref": "#/definitions/consoleAuthenticateLogoutRequest"
    +            }
    +          }
    +        ],
    +        "tags": [
    +          "Console"
    +        ]
    +      }
    +    },
         "/v2/console/config": {
           "get": {
             "summary": "Get server config and configuration warnings.",
    @@ -2998,6 +3031,16 @@
           },
           "title": "API Explorer List of Endpoints"
         },
    +    "consoleAuthenticateLogoutRequest": {
    +      "type": "object",
    +      "properties": {
    +        "token": {
    +          "type": "string",
    +          "description": "Session token to log out."
    +        }
    +      },
    +      "description": "Log out a session and invalidate a session token."
    +    },
         "consoleAuthenticateRequest": {
           "type": "object",
           "properties": {
    
  • console/ui/dist/index.html+2 2 modified
    @@ -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>
    
  • console/ui/dist/static/main.2778305943c5228ee227.js+1 1 renamed
  • console/ui/src/app/authentication-error.interceptor.ts+9 7 modified
    @@ -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}`);
           }
    
  • console/ui/src/app/authentication.service.ts+11 5 modified
    @@ -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';
    @@ -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 {
    
  • console/ui/src/app/base/base.component.ts+3 1 modified
    @@ -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 {
    
  • console/ui/src/app/console.service.ts+13 0 modified
    @@ -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.
    @@ -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`;
    
  • main.go+3 2 modified
    @@ -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)
    @@ -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"
    
  • server/console_authenticate.go+38 7 modified
    @@ -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"
    @@ -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"`
    @@ -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"])
    @@ -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
     		}
    @@ -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
    
  • server/console.go+22 7 modified
    @@ -19,6 +19,7 @@ import (
     	"crypto"
     	"database/sql"
     	"fmt"
    +	"github.com/gofrs/uuid"
     	"io/ioutil"
     	"math"
     	"net"
    @@ -136,6 +137,7 @@ type ConsoleServer struct {
     	router               MessageRouter
     	StreamManager        StreamManager
     	sessionCache         SessionCache
    +	consoleSessionCache  SessionCache
     	statusRegistry       *StatusRegistry
     	matchRegistry        MatchRegistry
     	statusHandler        StatusHandler
    @@ -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.
    @@ -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...)
     
    @@ -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,
    @@ -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 {
    @@ -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)
    @@ -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 "
     
    @@ -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"])
     			}
    @@ -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
    @@ -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)
     
    
  • server/console_storage_import.go+1 1 modified
    @@ -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 {
    
  • server/session_cache.go+2 2 modified
    @@ -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{
    @@ -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():
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.