Skip to content

Commit 91300cd

Browse files
authored
Merge pull request #61 from dkrizic/copilot/fix-login-with-any-password
Add stream interceptor for authentication
2 parents 637c2da + 31d0f65 commit 91300cd

2 files changed

Lines changed: 76 additions & 33 deletions

File tree

service/service/auth/auth.go

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,54 @@ func requiresAuthentication(fullMethod string) bool {
2828
return false
2929
}
3030

31+
// validateCredentials extracts and validates credentials from the context metadata
32+
func validateCredentials(ctx context.Context, fullMethod, username, password string) error {
33+
// Extract metadata from context
34+
md, ok := metadata.FromIncomingContext(ctx)
35+
if !ok {
36+
slog.WarnContext(ctx, "Missing metadata in request", "method", fullMethod)
37+
return status.Error(codes.Unauthenticated, "missing metadata")
38+
}
39+
40+
// Check for authorization header
41+
authHeaders := md.Get("authorization")
42+
if len(authHeaders) == 0 {
43+
slog.WarnContext(ctx, "Missing authorization header", "method", fullMethod)
44+
return status.Error(codes.Unauthenticated, "missing authorization header")
45+
}
46+
47+
// Parse Basic Auth header
48+
auth := authHeaders[0]
49+
if !strings.HasPrefix(auth, "Basic ") {
50+
slog.WarnContext(ctx, "Invalid authorization header format", "method", fullMethod)
51+
return status.Error(codes.Unauthenticated, "invalid authorization header")
52+
}
53+
54+
// Decode base64 credentials
55+
payload, err := base64.StdEncoding.DecodeString(auth[6:])
56+
if err != nil {
57+
slog.WarnContext(ctx, "Failed to decode authorization header", "method", fullMethod, "error", err)
58+
return status.Error(codes.Unauthenticated, "invalid authorization header")
59+
}
60+
61+
// Split username:password
62+
pair := strings.SplitN(string(payload), ":", 2)
63+
if len(pair) != 2 {
64+
slog.WarnContext(ctx, "Invalid credentials format", "method", fullMethod)
65+
return status.Error(codes.Unauthenticated, "invalid credentials format")
66+
}
67+
68+
// Validate credentials
69+
if pair[0] != username || pair[1] != password {
70+
slog.WarnContext(ctx, "Invalid credentials", "method", fullMethod, "username", pair[0])
71+
return status.Error(codes.Unauthenticated, "invalid credentials")
72+
}
73+
74+
// Credentials are valid
75+
slog.DebugContext(ctx, "Authentication successful", "method", fullMethod, "username", pair[0])
76+
return nil
77+
}
78+
3179
// SelectiveInterceptor creates a gRPC unary server interceptor that only applies
3280
// authentication to specific services (Feature and Workload)
3381
func SelectiveInterceptor(enabled bool, username, password string) grpc.UnaryServerInterceptor {
@@ -48,49 +96,42 @@ func SelectiveInterceptor(enabled bool, username, password string) grpc.UnarySer
4896
return handler(ctx, req)
4997
}
5098

51-
// Extract metadata from context
52-
md, ok := metadata.FromIncomingContext(ctx)
53-
if !ok {
54-
slog.WarnContext(ctx, "Missing metadata in request", "method", info.FullMethod)
55-
return nil, status.Error(codes.Unauthenticated, "missing metadata")
56-
}
57-
58-
// Check for authorization header
59-
authHeaders := md.Get("authorization")
60-
if len(authHeaders) == 0 {
61-
slog.WarnContext(ctx, "Missing authorization header", "method", info.FullMethod)
62-
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
99+
// Validate credentials
100+
if err := validateCredentials(ctx, info.FullMethod, username, password); err != nil {
101+
return nil, err
63102
}
64103

65-
// Parse Basic Auth header
66-
auth := authHeaders[0]
67-
if !strings.HasPrefix(auth, "Basic ") {
68-
slog.WarnContext(ctx, "Invalid authorization header format", "method", info.FullMethod)
69-
return nil, status.Error(codes.Unauthenticated, "invalid authorization header")
70-
}
104+
// Credentials are valid, proceed with the request
105+
return handler(ctx, req)
106+
}
107+
}
71108

72-
// Decode base64 credentials
73-
payload, err := base64.StdEncoding.DecodeString(auth[6:])
74-
if err != nil {
75-
slog.WarnContext(ctx, "Failed to decode authorization header", "method", info.FullMethod, "error", err)
76-
return nil, status.Error(codes.Unauthenticated, "invalid authorization header")
109+
// SelectiveStreamInterceptor creates a gRPC stream server interceptor that only applies
110+
// authentication to specific services (Feature and Workload)
111+
func SelectiveStreamInterceptor(enabled bool, username, password string) grpc.StreamServerInterceptor {
112+
return func(
113+
srv interface{},
114+
ss grpc.ServerStream,
115+
info *grpc.StreamServerInfo,
116+
handler grpc.StreamHandler,
117+
) error {
118+
// If authentication is not enabled, allow all requests
119+
if !enabled {
120+
return handler(srv, ss)
77121
}
78122

79-
// Split username:password
80-
pair := strings.SplitN(string(payload), ":", 2)
81-
if len(pair) != 2 {
82-
slog.WarnContext(ctx, "Invalid credentials format", "method", info.FullMethod)
83-
return nil, status.Error(codes.Unauthenticated, "invalid credentials format")
123+
// Only authenticate protected services (Feature and Workload)
124+
// Allow Health and Meta services to pass through without authentication
125+
if !requiresAuthentication(info.FullMethod) {
126+
return handler(srv, ss)
84127
}
85128

86129
// Validate credentials
87-
if pair[0] != username || pair[1] != password {
88-
slog.WarnContext(ctx, "Invalid credentials", "method", info.FullMethod, "username", pair[0])
89-
return nil, status.Error(codes.Unauthenticated, "invalid credentials")
130+
if err := validateCredentials(ss.Context(), info.FullMethod, username, password); err != nil {
131+
return err
90132
}
91133

92134
// Credentials are valid, proceed with the request
93-
slog.DebugContext(ctx, "Authentication successful", "method", info.FullMethod, "username", pair[0])
94-
return handler(ctx, req)
135+
return handler(srv, ss)
95136
}
96137
}

service/service/service.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,13 @@ func Service(ctx context.Context, cmd *cli.Command) error {
139139
))
140140

141141
authInterceptor := auth.SelectiveInterceptor(authEnabled, authUsername, authPassword)
142+
authStreamInterceptor := auth.SelectiveStreamInterceptor(authEnabled, authUsername, authPassword)
142143

143144
// Create gRPC server with chained interceptors
144145
grpcServer := grpc.NewServer(
145146
grpc.StatsHandler(otelInterceptor),
146147
grpc.ChainUnaryInterceptor(authInterceptor),
148+
grpc.ChainStreamInterceptor(authStreamInterceptor),
147149
)
148150

149151
// meta

0 commit comments

Comments
 (0)