Skip to content

Instantly share code, notes, and snippets.

@tcartwright
Last active May 27, 2025 16:49
Show Gist options
  • Save tcartwright/70ab30ada50b98ff5ad585cbf5e83053 to your computer and use it in GitHub Desktop.
Save tcartwright/70ab30ada50b98ff5ad585cbf5e83053 to your computer and use it in GitHub Desktop.
GENERAL: Claims Based Auth with Role Management

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:

  1. 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.

  2. 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:

  1. Retrieves the user's assigned roles.
  2. Expands each role into its associated claims.
  3. Applies any negated claims to exclude unwanted permissions.
  4. 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.

Effective Claims Calculation

Effective Claims = (Role Claims βˆͺ User Claims) - Negated Claims

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.

Claims-Based Auth Schema (SQL Server Example)

This example defines tables and stored procedures for implementing a flexible claims-based authentication system in SQL Server with:

  • Roles
  • Claims
  • Role-to-Claim mappings
  • User-to-Role mappings
  • Custom User Claims
  • Claim Negations (per-user)

πŸ—ƒοΈ Tables

-- Users
CREATE TABLE Users (
    UserId INT PRIMARY KEY,
    Username NVARCHAR(100) UNIQUE NOT NULL
);

-- Claims
CREATE TABLE Claims (
    ClaimId INT PRIMARY KEY,
    ClaimName NVARCHAR(100) UNIQUE NOT NULL
);

-- Roles
CREATE TABLE Roles (
    RoleId INT PRIMARY KEY,
    RoleName NVARCHAR(100) UNIQUE NOT NULL
);

-- Role to Claim Mapping
CREATE TABLE RoleClaims (
    RoleId INT,
    ClaimId INT,
    PRIMARY KEY (RoleId, ClaimId),
    FOREIGN KEY (RoleId) REFERENCES Roles(RoleId),
    FOREIGN KEY (ClaimId) REFERENCES Claims(ClaimId)
);

-- User to Role Mapping
CREATE TABLE UserRoles (
    UserId INT,
    RoleId INT,
    PRIMARY KEY (UserId, RoleId),
    FOREIGN KEY (UserId) REFERENCES Users(UserId),
    FOREIGN KEY (RoleId) REFERENCES Roles(RoleId)
);

-- Custom Claims Assigned Directly to Users
CREATE TABLE UserClaims (
    UserId INT,
    ClaimId INT,
    PRIMARY KEY (UserId, ClaimId),
    FOREIGN KEY (UserId) REFERENCES Users(UserId),
    FOREIGN KEY (ClaimId) REFERENCES Claims(ClaimId)
);

-- Claims Explicitly Negated for a User
CREATE TABLE UserClaimNegations (
    UserId INT,
    ClaimId INT,
    PRIMARY KEY (UserId, ClaimId),
    FOREIGN KEY (UserId) REFERENCES Users(UserId),
    FOREIGN KEY (ClaimId) REFERENCES Claims(ClaimId)
);

πŸ“œ 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 names
AS
BEGIN
    SET 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.ClaimId
    FROM @Claims temp
    JOIN Claims c ON c.ClaimName = temp.ClaimName;
END

βœ… AssignRoleToUser

Assigns an existing role to a user.

CREATE PROCEDURE AssignRoleToUser
    @UserId INT,
    @RoleName NVARCHAR(100)
AS
BEGIN
    SET 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)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @RoleId INT = (SELECT RoleId FROM Roles WHERE RoleName = @RoleName);
    IF @RoleId IS NULL
        THROW 50001, 'Role does not exist.', 1;

    DELETE FROM 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)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ClaimId INT = (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
    IF @ClaimId IS NULL
    BEGIN
        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)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ClaimId INT = (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
    IF @ClaimId IS NOT NULL
    BEGIN
        DELETE FROM 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)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ClaimId INT = (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
    IF @ClaimId IS NULL
    BEGIN
        INSERT 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)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ClaimId INT = (SELECT ClaimId FROM Claims WHERE ClaimName = @ClaimName);
    IF @ClaimId IS NOT NULL
    BEGIN
        DELETE FROM 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 INT
AS
BEGIN
    SET NOCOUNT ON;

    -- Claims from roles (excluding negated)
    SELECT DISTINCT c.ClaimName
    FROM UserRoles ur
    JOIN RoleClaims rc ON rc.RoleId = ur.RoleId
    JOIN Claims c ON c.ClaimId = rc.ClaimId
    WHERE ur.UserId = @UserId
    AND c.ClaimId NOT IN (
        SELECT ClaimId FROM UserClaimNegations WHERE UserId = @UserId
    )

    UNION

    -- Custom user claims
    SELECT c.ClaimName
    FROM UserClaims uc
    JOIN Claims c ON c.ClaimId = uc.ClaimId
    WHERE uc.UserId = @UserId;
END
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment