diff --git a/pkg/http/BUILD.bazel b/pkg/http/BUILD.bazel index f5525033..0161e7c0 100644 --- a/pkg/http/BUILD.bazel +++ b/pkg/http/BUILD.bazel @@ -33,6 +33,7 @@ go_library( "@org_golang_google_grpc//codes", "@org_golang_google_grpc//status", "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/known/durationpb", "@org_golang_google_protobuf//types/known/timestamppb", "@org_golang_x_oauth2//:oauth2", ], @@ -58,6 +59,7 @@ go_test( "@org_golang_google_grpc//codes", "@org_golang_google_grpc//status", "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/known/durationpb", "@org_golang_google_protobuf//types/known/structpb", "@org_golang_google_protobuf//types/known/timestamppb", "@org_golang_x_oauth2//:oauth2", diff --git a/pkg/http/oidc_authenticator.go b/pkg/http/oidc_authenticator.go index 0b02b513..c227b1d8 100644 --- a/pkg/http/oidc_authenticator.go +++ b/pkg/http/oidc_authenticator.go @@ -22,6 +22,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -121,7 +122,7 @@ func (a *oidcAuthenticator) setCookieValue(w http.ResponseWriter, cookieValue *o return nil } -func (a *oidcAuthenticator) getClaimsAndSetCookie(ctx context.Context, token *oauth2.Token, w http.ResponseWriter) (*auth.AuthenticationMetadata, error) { +func (a *oidcAuthenticator) getClaimsAndSetCookie(ctx context.Context, token *oauth2.Token, defaultExpiration time.Duration, w http.ResponseWriter) (*auth.AuthenticationMetadata, error) { // Obtain claims from the user info endpoint. claimsRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, a.userInfoURL, nil) if err != nil { @@ -157,14 +158,25 @@ func (a *oidcAuthenticator) getClaimsAndSetCookie(ctx context.Context, token *oa return nil, util.StatusWrap(err, "Failed to create authentication metadata") } + expiration := token.Expiry + if expiration.IsZero() { + // The access token response did not contain an + // expiration duration. Simply assume an expiration + // duration of 1 minute, growing exponentially as long + // as refreshing succeeds. + expiration = a.clock.Now().Add(defaultExpiration) + defaultExpiration *= 2 + } + // Redirect to the originally requested path, with the // authentication metadata stored in a cookie. if err := a.setCookieValue(w, &oidc.CookieValue{ SessionState: &oidc.CookieValue_Authenticated_{ Authenticated: &oidc.CookieValue_Authenticated{ AuthenticationMetadata: authenticationMetadata.GetFullProto(), - Expiration: timestamppb.New(token.Expiry), + Expiration: timestamppb.New(expiration), RefreshToken: token.RefreshToken, + DefaultExpiration: durationpb.New(defaultExpiration), }, }, }); err != nil { @@ -207,7 +219,7 @@ func (a *oidcAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) } // Redirect back to the originally requested page. - if _, err := a.getClaimsAndSetCookie(ctx, token, w); err != nil { + if _, err := a.getClaimsAndSetCookie(ctx, token, time.Minute, w); err != nil { return nil, err } http.Redirect(w, r, authenticating.OriginalRequestUri, http.StatusSeeOther) @@ -235,7 +247,7 @@ func (a *oidcAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) Expiry: time.Unix(0, 0), }, ).Token(); err == nil { - if authenticationMetadata, err := a.getClaimsAndSetCookie(ctx, refreshedToken, w); err == nil { + if authenticationMetadata, err := a.getClaimsAndSetCookie(ctx, refreshedToken, authenticated.DefaultExpiration.AsDuration(), w); err == nil { return authenticationMetadata, nil } } diff --git a/pkg/http/oidc_authenticator_test.go b/pkg/http/oidc_authenticator_test.go index 57b4806e..95c7197b 100644 --- a/pkg/http/oidc_authenticator_test.go +++ b/pkg/http/oidc_authenticator_test.go @@ -23,6 +23,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -131,6 +132,7 @@ func TestOIDCAuthenticator(t *testing.T) { Authenticated: &oidc.CookieValue_Authenticated{ AuthenticationMetadata: &auth.AuthenticationMetadata{}, Expiration: ×tamppb.Timestamp{Seconds: 1693145873}, + DefaultExpiration: &durationpb.Duration{Seconds: 60}, }, }, })...), nil @@ -175,7 +177,7 @@ func TestOIDCAuthenticator(t *testing.T) { }, w.HeaderMap) }) - t.Run("RegularRequestExpiredAccessTokenWithRefreshToken", func(t *testing.T) { + t.Run("RegularRequestExpiredAccessTokenWithRefreshTokenWithExpiresIn", func(t *testing.T) { // If the access token is expired and a refresh token is // available, we may be able to continue the session // without sending the user through the authorization @@ -193,6 +195,7 @@ func TestOIDCAuthenticator(t *testing.T) { AuthenticationMetadata: &auth.AuthenticationMetadata{}, Expiration: ×tamppb.Timestamp{Seconds: 1693147158}, RefreshToken: "RefreshToken1", + DefaultExpiration: &durationpb.Duration{Seconds: 60}, }, }, })...), nil @@ -252,6 +255,7 @@ func TestOIDCAuthenticator(t *testing.T) { AuthenticationMetadata: expectedAuthenticationMetadata, Expiration: ×tamppb.Timestamp{Seconds: 1693150813}, RefreshToken: "RefreshToken2", + DefaultExpiration: &durationpb.Duration{Seconds: 60}, }, }, }), @@ -276,6 +280,108 @@ func TestOIDCAuthenticator(t *testing.T) { }, w.HeaderMap) }) + t.Run("RegularRequestExpiredAccessTokenWithRefreshTokenWithoutExpiresIn", func(t *testing.T) { + // It is only recommended that the access token response + // contains an 'expires_in' value. If it does not, let's + // pick an exponentially growing expiration time. + // + // More details: RFC 6749, section 4.2.2. + cookieAEAD.EXPECT().Open( + gomock.Any(), + []byte{0x84, 0x4b, 0x47, 0xdd}, + []byte{0xac, 0x4f, 0xc2, 0x0d, 0x5b, 0x01, 0x78, 0x67}, + nil, + ).DoAndReturn(func(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + return append(dst, protoMustMarshal(&oidc.CookieValue{ + SessionState: &oidc.CookieValue_Authenticated_{ + Authenticated: &oidc.CookieValue_Authenticated{ + AuthenticationMetadata: &auth.AuthenticationMetadata{}, + Expiration: ×tamppb.Timestamp{Seconds: 1693631281}, + RefreshToken: "RefreshToken1", + DefaultExpiration: &durationpb.Duration{Seconds: 60}, + }, + }, + })...), nil + }) + clock.EXPECT().Now().Return(time.Unix(1693631288, 0)) + roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(r *http.Request) (*http.Response, error) { + require.Equal(t, "https://login.com/token", r.URL.String()) + r.ParseForm() + require.Equal(t, url.Values{ + "client_id": []string{"MyClientID"}, + "client_secret": []string{"MyClientSecret"}, + "grant_type": []string{"refresh_token"}, + "refresh_token": []string{"RefreshToken1"}, + }, r.Form) + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{ + "access_token": "AccessToken2", + "refresh_token": "RefreshToken2", + "token_type": "Bearer" + }`)), + }, nil + }) + clock.EXPECT().Now().Return(time.Unix(1693631289, 0)) + roundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn(func(r *http.Request) (*http.Response, error) { + require.Equal(t, "https://login.com/userinfo", r.URL.String()) + require.Equal(t, http.Header{ + "Authorization": []string{"Bearer AccessToken2"}, + }, r.Header) + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{ + "email": "john@myserver.com", + "name": "John Doe" + }`)), + }, nil + }) + nonce := []byte{0xa9, 0xe3, 0x9f, 0xc5} + expectRead(randomNumberGenerator, nonce) + expectedAuthenticationMetadata := &auth.AuthenticationMetadata{ + Public: structpb.NewStructValue(&structpb.Struct{ + Fields: map[string]*structpb.Value{ + "email": structpb.NewStringValue("john@myserver.com"), + "name": structpb.NewStringValue("John Doe"), + }, + }), + } + cookieAEAD.EXPECT().Seal( + gomock.Any(), + nonce, + protoMustMarshal(&oidc.CookieValue{ + SessionState: &oidc.CookieValue_Authenticated_{ + Authenticated: &oidc.CookieValue_Authenticated{ + AuthenticationMetadata: expectedAuthenticationMetadata, + Expiration: ×tamppb.Timestamp{Seconds: 1693631349}, + RefreshToken: "RefreshToken2", + DefaultExpiration: &durationpb.Duration{Seconds: 120}, + }, + }, + }), + nil, + ).DoAndReturn(func(dst, nonce, plaintext, additionalData []byte) []byte { + return append(dst, 0xfc, 0x39, 0x91, 0xcd, 0x66, 0xd1, 0xbb, 0x95) + }) + + w := httptest.NewRecorder() + r, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://myserver.com/hello.png", nil) + r.AddCookie(&http.Cookie{ + Name: "CookieName", + Value: "hEtH3axPwg1bAXhn", + }) + require.NoError(t, err) + metadata, err := authenticator.Authenticate(w, r) + require.NoError(t, err) + testutil.RequireEqualProto(t, expectedAuthenticationMetadata, metadata.GetFullProto()) + + require.Equal(t, http.Header{ + "Set-Cookie": []string{"CookieName=qeOfxfw5kc1m0buV; Path=/; HttpOnly; Secure; SameSite=Strict"}, + }, w.HeaderMap) + }) + t.Run("CallbackRequestWithoutCookie", func(t *testing.T) { // After authorization has been performed, the user is // sent to the redirect URL. We can only finalize the diff --git a/pkg/proto/http/oidc/BUILD.bazel b/pkg/proto/http/oidc/BUILD.bazel index 59921457..b3e155de 100644 --- a/pkg/proto/http/oidc/BUILD.bazel +++ b/pkg/proto/http/oidc/BUILD.bazel @@ -8,6 +8,7 @@ proto_library( visibility = ["//visibility:public"], deps = [ "//pkg/proto/auth:auth_proto", + "@com_google_protobuf//:duration_proto", "@com_google_protobuf//:timestamp_proto", ], ) diff --git a/pkg/proto/http/oidc/oidc.pb.go b/pkg/proto/http/oidc/oidc.pb.go index af698421..f47d6a54 100644 --- a/pkg/proto/http/oidc/oidc.pb.go +++ b/pkg/proto/http/oidc/oidc.pb.go @@ -10,6 +10,7 @@ import ( auth "github.com/buildbarn/bb-storage/pkg/proto/auth" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -166,6 +167,7 @@ type CookieValue_Authenticated struct { AuthenticationMetadata *auth.AuthenticationMetadata `protobuf:"bytes,1,opt,name=authentication_metadata,json=authenticationMetadata,proto3" json:"authentication_metadata,omitempty"` Expiration *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiration,proto3" json:"expiration,omitempty"` RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + DefaultExpiration *durationpb.Duration `protobuf:"bytes,4,opt,name=default_expiration,json=defaultExpiration,proto3" json:"default_expiration,omitempty"` } func (x *CookieValue_Authenticated) Reset() { @@ -221,17 +223,26 @@ func (x *CookieValue_Authenticated) GetRefreshToken() string { return "" } +func (x *CookieValue_Authenticated) GetDefaultExpiration() *durationpb.Duration { + if x != nil { + return x.DefaultExpiration + } + return nil +} + var File_pkg_proto_http_oidc_oidc_proto protoreflect.FileDescriptor var file_pkg_proto_http_oidc_oidc_proto_rawDesc = []byte{ 0x0a, 0x1e, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x62, 0x61, 0x72, 0x6e, 0x2e, 0x68, 0x74, 0x74, 0x70, - 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x90, 0x04, 0x0a, 0x0b, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x56, 0x61, 0x6c, 0x75, + 0x6f, 0x22, 0xda, 0x04, 0x0a, 0x0b, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x59, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x62, 0x61, 0x72, 0x6e, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, @@ -250,7 +261,7 @@ var file_pkg_proto_http_oidc_oidc_proto_rawDesc = []byte{ 0x14, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x55, 0x72, 0x69, 0x1a, - 0xd1, 0x01, 0x0a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x9b, 0x02, 0x0a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x5f, 0x0a, 0x17, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x62, 0x61, 0x72, 0x6e, 0x2e, 0x61, @@ -263,12 +274,16 @@ var file_pkg_proto_http_oidc_oidc_proto_rawDesc = []byte{ 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x42, 0x0f, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x62, 0x61, 0x72, 0x6e, 0x2f, 0x62, 0x62, 0x2d, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x6b, 0x65, 0x6e, 0x12, 0x48, 0x0a, 0x12, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, 0x64, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0f, 0x0a, + 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x42, 0x35, + 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x62, 0x61, 0x72, 0x6e, 0x2f, 0x62, 0x62, 0x2d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x74, 0x74, 0x70, + 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -290,17 +305,19 @@ var file_pkg_proto_http_oidc_oidc_proto_goTypes = []interface{}{ (*CookieValue_Authenticated)(nil), // 2: buildbarn.http.oidc.CookieValue.Authenticated (*auth.AuthenticationMetadata)(nil), // 3: buildbarn.auth.AuthenticationMetadata (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 5: google.protobuf.Duration } var file_pkg_proto_http_oidc_oidc_proto_depIdxs = []int32{ 1, // 0: buildbarn.http.oidc.CookieValue.authenticating:type_name -> buildbarn.http.oidc.CookieValue.Authenticating 2, // 1: buildbarn.http.oidc.CookieValue.authenticated:type_name -> buildbarn.http.oidc.CookieValue.Authenticated 3, // 2: buildbarn.http.oidc.CookieValue.Authenticated.authentication_metadata:type_name -> buildbarn.auth.AuthenticationMetadata 4, // 3: buildbarn.http.oidc.CookieValue.Authenticated.expiration:type_name -> google.protobuf.Timestamp - 4, // [4:4] is the sub-list for method output_type - 4, // [4:4] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 5, // 4: buildbarn.http.oidc.CookieValue.Authenticated.default_expiration:type_name -> google.protobuf.Duration + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_pkg_proto_http_oidc_oidc_proto_init() } diff --git a/pkg/proto/http/oidc/oidc.proto b/pkg/proto/http/oidc/oidc.proto index 5fe25808..cd37a0fb 100644 --- a/pkg/proto/http/oidc/oidc.proto +++ b/pkg/proto/http/oidc/oidc.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package buildbarn.http.oidc; +import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; import "pkg/proto/auth/auth.proto"; @@ -34,6 +35,10 @@ message CookieValue { // If set, a refresh token that may be used to obtain a new access // token and updated claims after the current access token expires. string refresh_token = 3; + + // The expiration duration to use if the next token refresh does not + // return "expires_in" explicitly. + google.protobuf.Duration default_expiration = 4; } oneof session_state {