1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-23 03:01:23 +01:00

[PM-6303] Add duo state to 2fa (#3806)

* add duo state to 2fa

* Id to UserId
This commit is contained in:
Jake Fink 2024-02-14 18:00:46 -05:00 committed by GitHub
parent 744d21ec5e
commit d99d3b8380
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 65 additions and 5 deletions

View File

@ -1,12 +1,14 @@
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Duo = DuoUniversal;
namespace Bit.Core.Auth.Identity;
/*
/*
PM-5156 addresses tech debt
Interface to allow for DI, will end up being removed as part of the removal of the old Duo SDK v2 flows.
This service is to support SDK v4 flows for Duo. At some time in the future we will need
@ -22,6 +24,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
{
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory;
/// <summary>
/// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK
@ -30,10 +33,12 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
/// <param name="globalSettings">used to fetch vault URL for Redirect URL</param>
public TemporaryDuoWebV4SDKService(
ICurrentContext currentContext,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory)
{
_currentContext = currentContext;
_globalSettings = globalSettings;
_tokenDataFactory = tokenDataFactory;
}
/// <summary>
@ -56,7 +61,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
return null;
}
var state = Duo.Client.GenerateState(); //? Not sure on this yet. But required for GenerateAuthUrl
var state = _tokenDataFactory.Protect(new DuoUserStateTokenable(user));
var authUrl = duoClient.GenerateAuthUri(user.Email, state);
return authUrl;
@ -82,8 +87,20 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
return false;
}
var parts = token.Split("|");
var authCode = parts[0];
var state = parts[1];
_tokenDataFactory.TryUnprotect(state, out var tokenable);
if (!tokenable.Valid || !tokenable.TokenIsValid(user))
{
return false;
}
// duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used
// their authCode with a victims credentials
var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email);
// If the result of the exchange doesn't throw an exception and it's not null, then it's valid
var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(token, user.Email);
return res.AuthResult.Result == "allow";
}
@ -100,7 +117,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
/// <returns>Duo.Client object or null</returns>
private async Task<Duo.Client> BuildDuoClientAsync(TwoFactorProvider provider)
{
// Fetch Client name from header value since duo auth can be initiated from multiple clients and we want
// Fetch Client name from header value since duo auth can be initiated from multiple clients and we want
// to redirect back to the correct client
_currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName);
var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}",

View File

@ -0,0 +1,37 @@
using Bit.Core.Entities;
using Bit.Core.Tokens;
using Newtonsoft.Json;
namespace Bit.Core.Auth.Models.Business.Tokenables;
public class DuoUserStateTokenable : Tokenable
{
public const string ClearTextPrefix = "BwDuoUserId";
public const string DataProtectorPurpose = "DuoUserIdTokenDataProtector";
public const string TokenIdentifier = "DuoUserIdToken";
public string Identifier { get; set; } = TokenIdentifier;
public Guid UserId { get; set; }
public override bool Valid => Identifier == TokenIdentifier &&
UserId != default;
[JsonConstructor]
public DuoUserStateTokenable()
{
}
public DuoUserStateTokenable(User user)
{
UserId = user?.Id ?? default;
}
public bool TokenIsValid(User user)
{
if (UserId == default || user == null)
{
return false;
}
return UserId == user.Id;
}
}

View File

@ -197,6 +197,12 @@ public static class ServiceCollectionExtensions
OrgUserInviteTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrgUserInviteTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<DuoUserStateTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<DuoUserStateTokenable>(
DuoUserStateTokenable.ClearTextPrefix,
DuoUserStateTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<DuoUserStateTokenable>>>()));
}
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)