You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Claims-Based Authentication with Roles, Negations, and Custom Claims
This approach uses claims-based authentication for the API, with roles as a convenient way to manage claims.
Each user or authenticating entity can be assigned one or more roles, and each role maps to a predefined set of claims that represent specific permissions (e.g., can_view_reports, can_edit_users, etc.).
Advanced Features
Two advanced features will be supported:
Claim Overrides (Negation)
Sometimes, a user shouldn't inherit a specific claim from a role. We allow claim-level overrides, where a claim inherited from a role can be explicitly negated for that user.
Custom User Claims
Users can also have custom claims assigned directly to them, independent of any role. This lets us fine-tune permissions on a per-user basis.
Claim Resolution Process
When a user logs in, the system:
Retrieves the user's assigned roles.
Expands each role into its associated claims.
Applies any negated claims to exclude unwanted permissions.
Adds any custom claims assigned directly to the user.
The resulting final set of claims is included in the user's token (e.g., JWT), which the API uses for authorization.
This approach gives us a flexible and centralized way to manage authorization policies at both the role and user level.
NOTE: The schema and stored procs were generated by AI. I will eventually replace them with versions I have tried and tested. However, until then YMMV.
π Stored Procedures for Claims-Based Authentication (SQL Server)
These stored procedures provide a complete interface for managing roles, claims, user assignments, custom claims, and claim negations.
β AddRoleWithClaims
Creates a new role and associates it with a list of claims.
CREATE PROCEDURE AddRoleWithClaims
@RoleName NVARCHAR(100),
@ClaimNames NVARCHAR(MAX) -- JSON array of claim namesASBEGINSET NOCOUNT ON;
INSERT INTO Roles (RoleName)
VALUES (@RoleName);
DECLARE @RoleId INT= SCOPE_IDENTITY();
DECLARE @Claims TABLE (ClaimName NVARCHAR(100));
INSERT INTO @Claims (ClaimName)
SELECT value FROM OPENJSON(@ClaimNames);
INSERT INTO Claims (ClaimName)
SELECT ClaimName
FROM @Claims
WHERE ClaimName NOT IN (SELECT ClaimName FROM Claims);
INSERT INTO RoleClaims (RoleId, ClaimId)
SELECT @RoleId, c.ClaimIdFROM @Claims temp
JOIN Claims c ONc.ClaimName=temp.ClaimName;
END
β AssignRoleToUser
Assigns an existing role to a user.
CREATE PROCEDURE AssignRoleToUser
@UserId INT,
@RoleName NVARCHAR(100)
ASBEGINSET NOCOUNT ON;
DECLARE @RoleId INT= (SELECT RoleId FROM Roles WHERE RoleName = @RoleName);
IF @RoleId IS NULL
THROW 50001, 'Role does not exist.', 1;
INSERT INTO UserRoles (UserId, RoleId)
VALUES (@UserId, @RoleId);
END
β RemoveRoleFromUser
Removes a role from a user.
CREATE PROCEDURE RemoveRoleFromUser
@UserId INT,
@RoleName NVARCHAR(100)
ASBEGINSET NOCOUNT ON;
DECLARE @RoleId INT= (SELECT RoleId FROM Roles WHERE RoleName = @RoleName);
IF @RoleId IS NULL
THROW 50001, 'Role does not exist.', 1;
DELETEFROM UserRoles
WHERE UserId = @UserId AND RoleId = @RoleId;
END
β AddCustomClaimToUser
Adds a custom claim directly to a user.
CREATE PROCEDURE AddCustomClaimToUser
@UserId INT,
@ClaimName NVARCHAR(100)
ASBEGINSET NOCOUNT ON;
DECLARE @ClaimId INT= (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
IF @ClaimId IS NULLBEGIN
THROW 50001, 'Claim does not exist.', 1;
END
INSERT INTO UserClaims (UserId, ClaimId)
VALUES (@UserId, @ClaimId);
END
β RemoveCustomClaimFromUser
Removes a custom claim from a user.
CREATE PROCEDURE RemoveCustomClaimFromUser
@UserId INT,
@ClaimName NVARCHAR(100)
ASBEGINSET NOCOUNT ON;
DECLARE @ClaimId INT= (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
IF @ClaimId IS NOT NULLBEGINDELETEFROM UserClaims
WHERE UserId = @UserId AND ClaimId = @ClaimId;
END
END
β NegateClaimForUser
Negates a claim for a user, excluding it from their effective permissions.
CREATE PROCEDURE NegateClaimForUser
@UserId INT,
@ClaimName NVARCHAR(100)
ASBEGINSET NOCOUNT ON;
DECLARE @ClaimId INT= (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
IF @ClaimId IS NULLBEGININSERT INTO Claims (ClaimName) VALUES (@ClaimName);
SET @ClaimId = SCOPE_IDENTITY();
END
INSERT INTO UserClaimNegations (UserId, ClaimId)
VALUES (@UserId, @ClaimId);
END
β RemoveNegatedClaimFromUser
Removes a negated claim so it can take effect again via roles or user claims.
CREATE PROCEDURE RemoveNegatedClaimFromUser
@UserId INT,
@ClaimName NVARCHAR(100)
ASBEGINSET NOCOUNT ON;
DECLARE @ClaimId INT= (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
IF @ClaimId IS NOT NULLBEGINDELETEFROM UserClaimNegations
WHERE UserId = @UserId AND ClaimId = @ClaimId;
END
END
π GetEffectiveClaimsForUser
Returns the final set of claims for a user, factoring in roles, custom claims, and negations.
CREATE PROCEDURE GetEffectiveClaimsForUser
@UserId INTASBEGINSET NOCOUNT ON;
-- Claims from roles (excluding negated)SELECT DISTINCTc.ClaimNameFROM UserRoles ur
JOIN RoleClaims rc ONrc.RoleId=ur.RoleIdJOIN Claims c ONc.ClaimId=rc.ClaimIdWHEREur.UserId= @UserId
ANDc.ClaimId NOT IN (
SELECT ClaimId FROM UserClaimNegations WHERE UserId = @UserId
)
UNION-- Custom user claimsSELECTc.ClaimNameFROM UserClaims uc
JOIN Claims c ONc.ClaimId=uc.ClaimIdWHEREuc.UserId= @UserId;
END