mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-20595] Add Policy for Send access (#6282)
* feat: add policy to API startup and Policies class to hold the static strings * test: add snapshot testing for constants to help with rust mappings * doc: add docs for send access
This commit is contained in:
@@ -33,6 +33,7 @@ using Bit.Core.Auth.Models.Api.Request;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Tools.SendFeatures;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.Identity;
|
||||
|
||||
|
||||
#if !OSS
|
||||
@@ -145,6 +146,12 @@ public class Startup
|
||||
(c.Value.Contains(ApiScopes.Api) || c.Value.Contains(ApiScopes.ApiSecrets))
|
||||
));
|
||||
});
|
||||
config.AddPolicy(Policies.Send, configurePolicy: policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiSendAccess);
|
||||
policy.RequireClaim(Claims.SendAccessClaims.SendId);
|
||||
});
|
||||
});
|
||||
|
||||
services.AddScoped<AuthenticatorTokenProvider>();
|
||||
|
||||
10
src/Core/Auth/Identity/Policies.cs
Normal file
10
src/Core/Auth/Identity/Policies.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
|
||||
public static class Policies
|
||||
{
|
||||
/// <summary>
|
||||
/// Policy for managing access to the Send feature.
|
||||
/// </summary>
|
||||
public const string Send = "Send"; // [Authorize(Policy = Policies.Send)]
|
||||
// TODO: migrate other existing policies to use this class
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
|
||||
/// <summary>
|
||||
/// String constants for the Send Access user feature
|
||||
/// Most of these need to be synced with the `bitwarden-auth` crate in the SDK.
|
||||
/// There is snapshot testing to help ensure this.
|
||||
/// </summary>
|
||||
public static class SendAccessConstants
|
||||
{
|
||||
@@ -41,7 +43,7 @@ public static class SendAccessConstants
|
||||
/// <summary>
|
||||
/// The sendId is missing from the request.
|
||||
/// </summary>
|
||||
public const string MissingSendId = "send_id_required";
|
||||
public const string SendIdRequired = "send_id_required";
|
||||
/// <summary>
|
||||
/// The sendId is invalid, does not match a known send.
|
||||
/// </summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ public class SendAccessGrantValidator(
|
||||
private static readonly Dictionary<string, string>
|
||||
_sendGrantValidatorErrorDescriptions = new()
|
||||
{
|
||||
{ SendAccessConstants.GrantValidatorResults.MissingSendId, $"{SendAccessConstants.TokenRequest.SendId} is required." },
|
||||
{ SendAccessConstants.GrantValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." },
|
||||
{ SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." }
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ public class SendAccessGrantValidator(
|
||||
// if the sendId is null then the request is the wrong shape and the request is invalid
|
||||
if (sendId == null)
|
||||
{
|
||||
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.MissingSendId);
|
||||
return (Guid.Empty, SendAccessConstants.GrantValidatorResults.SendIdRequired);
|
||||
}
|
||||
// the send_id is not null so the request is the correct shape, so we will attempt to parse it
|
||||
try
|
||||
@@ -125,7 +125,7 @@ public class SendAccessGrantValidator(
|
||||
return error switch
|
||||
{
|
||||
// Request is the wrong shape
|
||||
SendAccessConstants.GrantValidatorResults.MissingSendId => new GrantValidationResult(
|
||||
SendAccessConstants.GrantValidatorResults.SendIdRequired => new GrantValidationResult(
|
||||
TokenRequestErrors.InvalidRequest,
|
||||
errorDescription: _sendGrantValidatorErrorDescriptions[error],
|
||||
customResponse),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
Send Access Request Validation
|
||||
===
|
||||
|
||||
This feature supports the ability of Tools to require specific claims for access to sends.
|
||||
|
||||
In order to access Send data a user must meet the requirements laid out in these request validators.
|
||||
|
||||
# ***Important: String Constants***
|
||||
|
||||
The string constants contained herein are used in conjunction with the Auth module in the SDK. Any change to these string values _must_ be intentional and _must_ have a corresponding change in the SDK.
|
||||
|
||||
There is snapshot testing that will fail if the strings change to help detect unintended changes to the string constants.
|
||||
|
||||
# Custom Claims
|
||||
|
||||
Send access tokens contain custom claims specific to the Send the Send grant type.
|
||||
|
||||
1. `send_id` - is always included in the issued access token. This is the `GUID` of the request Send.
|
||||
1. `send_email` - only set when the Send requires `EmailOtp` authentication type.
|
||||
1. `type` - this will always be `Send`
|
||||
|
||||
# Authentication methods
|
||||
|
||||
## `NeverAuthenticate`
|
||||
|
||||
For a Send to be in this state two things can be true:
|
||||
1. The Send has been modified and no longer allows access.
|
||||
2. The Send does not exist.
|
||||
|
||||
## `NotAuthenticated`
|
||||
|
||||
In this scenario the Send is not protected by any added authentication or authorization and the access token is issued to the requesting user.
|
||||
|
||||
## `ResourcePassword`
|
||||
|
||||
In this scenario the Send is password protected and a user must supply the correct password hash to be issued an access token.
|
||||
|
||||
## `EmailOtp`
|
||||
|
||||
In this scenario the Send is only accessible to owners of specific email addresses. The user must submit a correct email. Once the email has been entered then ownership of the email must be established via OTP. The Otp is sent to the aforementioned email and must be supplied, along with the email, to be issued an access token.
|
||||
|
||||
# Send Access Request Validation
|
||||
|
||||
## Required Parameters
|
||||
|
||||
### All Requests
|
||||
- `send_id` - Base64 URL-encoded GUID of the send being accessed
|
||||
|
||||
### Password Protected Sends
|
||||
- `password_hash_b64` - client hashed Base64-encoded password.
|
||||
|
||||
### Email OTP Protected Sends
|
||||
- `email` - Email address associated with the send
|
||||
- `otp` - One-time password (optional - if missing, OTP is generated and sent)
|
||||
|
||||
## Error Responses
|
||||
|
||||
All errors include a custom response field:
|
||||
```json
|
||||
{
|
||||
"error": "invalid_request|invalid_grant",
|
||||
"error_description": "Human readable description",
|
||||
"send_access_error_type": "specific_error_code"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using Bit.Identity.IdentityServer.RequestValidators.SendAccess;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer.SendAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot tests to ensure the string constants in <see cref="SendAccessConstants"/> do not change unintentionally.
|
||||
/// If you change any of these values, please ensure you understand the impact and update the SDK accordingly.
|
||||
/// If you intentionally change any of these values, please update the tests to reflect the new expected values.
|
||||
/// </summary>
|
||||
public class SendConstantsSnapshotTests
|
||||
{
|
||||
[Fact]
|
||||
public void SendAccessError_Constant_HasCorrectValue()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("send_access_error_type", SendAccessConstants.SendAccessError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TokenRequest_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("send_id", SendAccessConstants.TokenRequest.SendId);
|
||||
Assert.Equal("password_hash_b64", SendAccessConstants.TokenRequest.ClientB64HashedPassword);
|
||||
Assert.Equal("email", SendAccessConstants.TokenRequest.Email);
|
||||
Assert.Equal("otp", SendAccessConstants.TokenRequest.Otp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrantValidatorResults_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("valid_send_guid", SendAccessConstants.GrantValidatorResults.ValidSendGuid);
|
||||
Assert.Equal("send_id_required", SendAccessConstants.GrantValidatorResults.SendIdRequired);
|
||||
Assert.Equal("send_id_invalid", SendAccessConstants.GrantValidatorResults.InvalidSendId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PasswordValidatorResults_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("password_hash_b64_invalid", SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch);
|
||||
Assert.Equal("password_hash_b64_required", SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmailOtpValidatorResults_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("email_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
|
||||
Assert.Equal("email_required", SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
|
||||
Assert.Equal("email_and_otp_required_otp_sent", SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent);
|
||||
Assert.Equal("otp_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid);
|
||||
Assert.Equal("otp_generation_failed", SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OtpToken_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("send_access", SendAccessConstants.OtpToken.TokenProviderName);
|
||||
Assert.Equal("email_otp", SendAccessConstants.OtpToken.Purpose);
|
||||
Assert.Equal("{0}_{1}", SendAccessConstants.OtpToken.TokenUniqueIdentifier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OtpEmail_Constants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("Your Bitwarden Send verification code is {0}", SendAccessConstants.OtpEmail.Subject);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user