Skip to content

Commit 9e519c5

Browse files
authored
refactor: remove deprecated tenant scoped methods (#34)
1 parent d4bcb8e commit 9e519c5

File tree

3 files changed

+0
-336
lines changed

3 files changed

+0
-336
lines changed

internal/api/handlers.go

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -205,102 +205,6 @@ func (h *Handlers) FinishWebAuthnLogin(c *gin.Context) {
205205
c.JSON(200, resp)
206206
}
207207

208-
// Tenant-scoped WebAuthn handlers
209-
// Deprecated: These handlers were used for path-based tenant routing (/t/{tenantID}/...).
210-
// Use the global handlers with X-Tenant-ID header instead.
211-
// See docs/adr/011-multi-tenancy.md for the new design.
212-
213-
// StartTenantWebAuthnLogin begins WebAuthn login for a specific tenant
214-
// Deprecated: Use StartWebAuthnLogin with X-Tenant-ID header instead.
215-
func (h *Handlers) StartTenantWebAuthnLogin(c *gin.Context) {
216-
if h.services.WebAuthn == nil {
217-
c.JSON(503, gin.H{"error": "WebAuthn not available"})
218-
return
219-
}
220-
221-
tenantID, ok := h.getTenantID(c)
222-
if !ok {
223-
c.JSON(500, gin.H{"error": "Tenant context not available"})
224-
return
225-
}
226-
227-
resp, err := h.services.WebAuthn.BeginTenantLogin(c.Request.Context(), tenantID)
228-
if err != nil {
229-
h.logger.Error("Failed to start tenant WebAuthn login",
230-
zap.Error(err),
231-
zap.String("tenant_id", string(tenantID)))
232-
c.JSON(500, gin.H{"error": "Failed to start login"})
233-
return
234-
}
235-
236-
c.JSON(200, resp)
237-
}
238-
239-
// FinishTenantWebAuthnLogin completes WebAuthn login for a specific tenant
240-
// Deprecated: Use FinishWebAuthnLogin with X-Tenant-ID header instead.
241-
func (h *Handlers) FinishTenantWebAuthnLogin(c *gin.Context) {
242-
if h.services.WebAuthn == nil {
243-
c.JSON(503, gin.H{"error": "WebAuthn not available"})
244-
return
245-
}
246-
247-
tenantID, ok := h.getTenantID(c)
248-
if !ok {
249-
c.JSON(500, gin.H{"error": "Tenant context not available"})
250-
return
251-
}
252-
253-
var req service.FinishLoginRequest
254-
if err := c.ShouldBindJSON(&req); err != nil {
255-
c.JSON(400, gin.H{"error": err.Error()})
256-
return
257-
}
258-
259-
resp, err := h.services.WebAuthn.FinishTenantLogin(c.Request.Context(), tenantID, &req)
260-
if err != nil {
261-
h.logger.Error("Failed to finish tenant WebAuthn login",
262-
zap.Error(err),
263-
zap.String("tenant_id", string(tenantID)))
264-
265-
// Check for redirect error - user belongs to a different tenant
266-
if redirectErr, ok := service.IsTenantRedirectError(err); ok {
267-
h.logger.Info("Tenant redirect required",
268-
zap.String("correct_tenant", string(redirectErr.CorrectTenantID)),
269-
zap.String("requested_tenant", string(tenantID)))
270-
c.JSON(409, gin.H{
271-
"error": "Tenant mismatch - redirect required",
272-
"redirect_tenant": string(redirectErr.CorrectTenantID),
273-
"user_id": redirectErr.UserID.String(),
274-
})
275-
return
276-
}
277-
278-
switch {
279-
case errors.Is(err, service.ErrChallengeNotFound):
280-
c.JSON(404, gin.H{"error": "Challenge not found"})
281-
case errors.Is(err, service.ErrChallengeExpired):
282-
c.JSON(410, gin.H{"error": "Challenge expired"})
283-
case errors.Is(err, service.ErrUserNotFound):
284-
c.JSON(404, gin.H{"error": "User not found"})
285-
case errors.Is(err, service.ErrCredentialNotFound):
286-
c.JSON(404, gin.H{"error": "Credential not found"})
287-
case errors.Is(err, service.ErrVerificationFailed):
288-
c.JSON(401, gin.H{"error": "Authentication failed"})
289-
case errors.Is(err, service.ErrTenantMismatch):
290-
c.JSON(403, gin.H{"error": "Credential belongs to a different tenant"})
291-
default:
292-
c.JSON(500, gin.H{"error": "Failed to complete login"})
293-
}
294-
return
295-
}
296-
297-
if len(resp.PrivateData) > 0 {
298-
c.Header("X-Private-Data-ETag", domain.ComputePrivateDataETag(resp.PrivateData))
299-
}
300-
301-
c.JSON(200, resp)
302-
}
303-
304208
// Storage handlers - Credentials
305209

306210
// getHolderDID retrieves the holder DID from context

internal/api/handlers_test.go

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -458,36 +458,6 @@ func TestHandlers_FinishWebAuthnRegistration_NotAvailable(t *testing.T) {
458458
}
459459
}
460460

461-
func TestHandlers_StartTenantWebAuthnLogin_NotAvailable(t *testing.T) {
462-
handlers, router := setupTestHandlers(t)
463-
handlers.services.WebAuthn = nil
464-
router.POST("/tenant/webauthn/login/start", handlers.StartTenantWebAuthnLogin)
465-
466-
w := httptest.NewRecorder()
467-
req := httptest.NewRequest(http.MethodPost, "/tenant/webauthn/login/start", strings.NewReader(`{}`))
468-
req.Header.Set("Content-Type", "application/json")
469-
router.ServeHTTP(w, req)
470-
471-
if w.Code != http.StatusServiceUnavailable {
472-
t.Errorf("Expected status %d, got %d", http.StatusServiceUnavailable, w.Code)
473-
}
474-
}
475-
476-
func TestHandlers_FinishTenantWebAuthnLogin_NotAvailable(t *testing.T) {
477-
handlers, router := setupTestHandlers(t)
478-
handlers.services.WebAuthn = nil
479-
router.POST("/tenant/webauthn/login/finish", handlers.FinishTenantWebAuthnLogin)
480-
481-
w := httptest.NewRecorder()
482-
req := httptest.NewRequest(http.MethodPost, "/tenant/webauthn/login/finish", strings.NewReader(`{}`))
483-
req.Header.Set("Content-Type", "application/json")
484-
router.ServeHTTP(w, req)
485-
486-
if w.Code != http.StatusServiceUnavailable {
487-
t.Errorf("Expected status %d, got %d", http.StatusServiceUnavailable, w.Code)
488-
}
489-
}
490-
491461
// Test credential storage handlers with authentication context
492462
func TestHandlers_StoreCredential_Unauthorized(t *testing.T) {
493463
handlers, router := setupTestHandlers(t)

internal/service/webauthn.go

Lines changed: 0 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,216 +1178,6 @@ func (r *credentialReader) Read(p []byte) (n int, err error) {
11781178
return n, nil
11791179
}
11801180

1181-
// =============================================================================
1182-
// Tenant-scoped WebAuthn methods
1183-
// =============================================================================
1184-
1185-
// BeginTenantLogin starts WebAuthn login for a specific tenant
1186-
func (s *WebAuthnService) BeginTenantLogin(ctx context.Context, tenantID domain.TenantID) (*BeginLoginResponse, error) {
1187-
// Generate login options without credentials (discoverable credentials mode)
1188-
_, session, err := s.webauthn.BeginDiscoverableLogin()
1189-
if err != nil {
1190-
s.logger.Error("Failed to begin tenant login", zap.Error(err))
1191-
return nil, fmt.Errorf("failed to begin login: %w", err)
1192-
}
1193-
1194-
// Store challenge with tenant ID
1195-
challengeID := generateChallengeID()
1196-
challenge := &domain.WebauthnChallenge{
1197-
ID: challengeID,
1198-
TenantID: string(tenantID),
1199-
Challenge: session.Challenge,
1200-
Action: "login",
1201-
ExpiresAt: time.Now().Add(5 * time.Minute),
1202-
}
1203-
1204-
if err := s.store.Challenges().Create(ctx, challenge); err != nil {
1205-
s.logger.Error("Failed to store challenge", zap.Error(err))
1206-
return nil, fmt.Errorf("failed to store challenge: %w", err)
1207-
}
1208-
1209-
// Build response
1210-
challengeBytes, err := base64.RawURLEncoding.DecodeString(session.Challenge)
1211-
if err != nil {
1212-
return nil, fmt.Errorf("failed to decode challenge: %w", err)
1213-
}
1214-
1215-
getOptions := GetOptionsResponse{
1216-
PublicKey: PublicKeyCredentialRequestOptions{
1217-
Challenge: challengeBytes,
1218-
RPId: s.cfg.Server.RPID,
1219-
UserVerification: protocol.VerificationRequired,
1220-
AllowCredentials: []PublicKeyCredentialDescriptor{},
1221-
},
1222-
}
1223-
1224-
return &BeginLoginResponse{
1225-
ChallengeID: challengeID,
1226-
GetOptions: getOptions,
1227-
}, nil
1228-
}
1229-
1230-
// FinishTenantLogin completes WebAuthn login for a specific tenant
1231-
// It validates that the user handle contains the expected tenant ID prefix
1232-
func (s *WebAuthnService) FinishTenantLogin(ctx context.Context, tenantID domain.TenantID, req *FinishLoginRequest) (*FinishLoginResponse, error) {
1233-
// Retrieve challenge
1234-
challenge, err := s.store.Challenges().GetByID(ctx, req.ChallengeID)
1235-
if err != nil {
1236-
if errors.Is(err, storage.ErrNotFound) {
1237-
return nil, ErrChallengeNotFound
1238-
}
1239-
return nil, fmt.Errorf("failed to retrieve challenge: %w", err)
1240-
}
1241-
1242-
// Check expiration
1243-
if time.Now().After(challenge.ExpiresAt) {
1244-
return nil, ErrChallengeExpired
1245-
}
1246-
1247-
// Verify tenant matches
1248-
if challenge.TenantID != string(tenantID) {
1249-
s.logger.Warn("Tenant mismatch in login challenge",
1250-
zap.String("expected", string(tenantID)),
1251-
zap.String("actual", challenge.TenantID))
1252-
return nil, ErrTenantMismatch
1253-
}
1254-
1255-
// Parse credential response
1256-
reader := newCredentialReader(req.Credential)
1257-
parsedResponse, err := protocol.ParseCredentialRequestResponseBody(reader)
1258-
if err != nil {
1259-
s.logger.Error("Failed to parse credential", zap.Error(err))
1260-
return nil, fmt.Errorf("failed to parse credential: %w", err)
1261-
}
1262-
1263-
// Decode user ID from user handle
1264-
// The new binary format (v1) doesn't include recoverable tenant ID,
1265-
// so we validate tenant membership after looking up the user.
1266-
userHandle := parsedResponse.Response.UserHandle
1267-
userID, err := domain.UserIDFromHandle(userHandle)
1268-
if err != nil {
1269-
// For backward compatibility, try treating the user handle as just a user ID
1270-
s.logger.Debug("User handle decode failed, treating as legacy user",
1271-
zap.String("user_handle", string(userHandle)),
1272-
zap.Error(err))
1273-
userID = domain.UserIDFromUserHandle(userHandle)
1274-
}
1275-
1276-
// Look up user
1277-
user, err := s.store.Users().GetByID(ctx, userID)
1278-
if err != nil {
1279-
if errors.Is(err, storage.ErrNotFound) {
1280-
return nil, ErrUserNotFound
1281-
}
1282-
return nil, fmt.Errorf("failed to retrieve user: %w", err)
1283-
}
1284-
1285-
// Get the user's actual tenant memberships for redirect support
1286-
userTenants, err := s.store.UserTenants().GetUserTenants(ctx, userID)
1287-
if err != nil {
1288-
s.logger.Warn("Failed to get user tenant memberships", zap.Error(err))
1289-
userTenants = []domain.TenantID{}
1290-
}
1291-
1292-
// Verify the user is a member of the requested tenant
1293-
isMember := false
1294-
for _, tid := range userTenants {
1295-
if tid == tenantID {
1296-
isMember = true
1297-
break
1298-
}
1299-
}
1300-
1301-
if !isMember {
1302-
// User is not a member of the requested tenant.
1303-
// Return a redirect error with the correct tenant if available.
1304-
s.logger.Warn("User not a member of requested tenant",
1305-
zap.String("user_id", userID.String()),
1306-
zap.String("requested_tenant", string(tenantID)),
1307-
zap.Any("user_tenants", userTenants))
1308-
1309-
if len(userTenants) > 0 {
1310-
// Return redirect error with the user's actual tenant
1311-
return nil, &TenantRedirectError{
1312-
CorrectTenantID: userTenants[0],
1313-
UserID: userID,
1314-
}
1315-
}
1316-
// User has no tenant memberships - this shouldn't happen normally
1317-
return nil, ErrTenantMismatch
1318-
}
1319-
1320-
// Find the credential
1321-
// Encode RawID to base64url to match the format used during credential storage
1322-
credentialID := base64.RawURLEncoding.EncodeToString(parsedResponse.RawID)
1323-
var foundCred *domain.WebauthnCredential
1324-
for i := range user.WebauthnCredentials {
1325-
if user.WebauthnCredentials[i].ID == credentialID {
1326-
foundCred = &user.WebauthnCredentials[i]
1327-
break
1328-
}
1329-
}
1330-
1331-
if foundCred == nil {
1332-
return nil, ErrCredentialNotFound
1333-
}
1334-
1335-
// Verify with the correct tenant-scoped user handle
1336-
waUser := &TenantWebAuthnUser{user: user, userHandle: domain.EncodeUserHandle(tenantID, userID)}
1337-
sessionData := webauthn.SessionData{
1338-
Challenge: challenge.Challenge,
1339-
RelyingPartyID: s.cfg.Server.RPID,
1340-
AllowedCredentialIDs: [][]byte{},
1341-
Expires: challenge.ExpiresAt,
1342-
UserVerification: protocol.VerificationRequired,
1343-
// UserID intentionally left empty for discoverable login
1344-
}
1345-
1346-
// Verify credential
1347-
_, err = s.webauthn.ValidateDiscoverableLogin(func(rawID, userHandle []byte) (webauthn.User, error) {
1348-
return waUser, nil
1349-
}, sessionData, parsedResponse)
1350-
1351-
if err != nil {
1352-
s.logger.Error("Failed to verify credential", zap.Error(err))
1353-
return nil, ErrVerificationFailed
1354-
}
1355-
1356-
// Generate token with tenant_id included for security boundary
1357-
token, err := s.generateToken(user, tenantID)
1358-
if err != nil {
1359-
return nil, fmt.Errorf("failed to generate token: %w", err)
1360-
}
1361-
1362-
// Clean up challenge
1363-
_ = s.store.Challenges().Delete(ctx, req.ChallengeID)
1364-
1365-
// Get tenant display name for the response
1366-
var tenantDisplayName string
1367-
if tenant, err := s.store.Tenants().GetByID(ctx, tenantID); err == nil {
1368-
tenantDisplayName = tenant.DisplayName
1369-
}
1370-
1371-
s.logger.Info("Completed tenant login",
1372-
zap.String("user_id", userID.String()),
1373-
zap.String("tenant_id", string(tenantID)))
1374-
1375-
displayName := ""
1376-
if user.DisplayName != nil {
1377-
displayName = *user.DisplayName
1378-
}
1379-
1380-
return &FinishLoginResponse{
1381-
UUID: userID.String(),
1382-
Token: token,
1383-
DisplayName: displayName,
1384-
PrivateData: user.PrivateData,
1385-
WebauthnRpId: s.cfg.Server.RPID,
1386-
TenantID: string(tenantID),
1387-
TenantDisplayName: tenantDisplayName,
1388-
}, nil
1389-
}
1390-
13911181
// TenantWebAuthnUser wraps a user with a tenant-scoped user handle
13921182
type TenantWebAuthnUser struct {
13931183
user *domain.User

0 commit comments

Comments
 (0)