Skip to content

Commit

Permalink
feat: add Client Credentials Grant auth support (#799)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwwoda committed Feb 7, 2022
1 parent a775e1e commit b8a64ca
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 1 deletion.
120 changes: 120 additions & 0 deletions Box.V2.Test/BoxCCGAuthTest.cs
@@ -0,0 +1,120 @@
using System;
using System.Threading.Tasks;
using Box.V2.Auth;
using Box.V2.CCGAuth;
using Box.V2.Config;
using Box.V2.Request;
using Box.V2.Services;
using Box.V2.Test.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Box.V2.Test
{
[TestClass]
public class BoxCCGAuthTest : BoxResourceManagerTest
{
private readonly Mock<IRequestHandler> _handler;
private readonly IBoxService _service;
private readonly Mock<IBoxConfig> _boxConfig;
private readonly BoxCCGAuth _ccgAuth;

public BoxCCGAuthTest()
{
// Initial Setup
_handler = new Mock<IRequestHandler>();
_service = new BoxService(_handler.Object);
_boxConfig = new Mock<IBoxConfig>();
_boxConfig.SetupGet(x => x.EnterpriseId).Returns("12345");
_boxConfig.SetupGet(x => x.ClientId).Returns("123");
_boxConfig.SetupGet(x => x.ClientSecret).Returns("SECRET");
_boxConfig.SetupGet(x => x.BoxApiHostUri).Returns(new Uri(Constants.BoxApiHostUriString));
_boxConfig.SetupGet(x => x.BoxAuthTokenApiUri).Returns(new Uri(Constants.BoxAuthTokenApiUriString));
_ccgAuth = new BoxCCGAuth(_boxConfig.Object, _service);
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public async Task GetAdminToken_ValidSession()
{
// Arrange
IBoxRequest boxRequest = null;
_handler.Setup(h => h.ExecuteAsync<OAuthSession>(It.IsAny<BoxRequest>()))
.Returns(Task<IBoxResponse<OAuthSession>>.Factory.StartNew(() => new BoxResponse<OAuthSession>()
{
Status = ResponseStatus.Success,
ContentString = "{\"access_token\":\"T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl\",\"expires_in\":3600,\"restricted_to\":[],\"token_type\":\"bearer\"}"
}))
.Callback<IBoxRequest>(r => boxRequest = r);

// Act
var accessToken = await _ccgAuth.AdminTokenAsync();

// Assert
Assert.AreEqual("https://api.box.com/oauth2/token", boxRequest.AbsoluteUri.AbsoluteUri);
Assert.AreEqual(RequestMethod.Post, boxRequest.Method);
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("grant_type", "client_credentials"));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("client_id", _boxConfig.Object.ClientId));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("client_secret", _boxConfig.Object.ClientSecret));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("box_subject_type", "enterprise"));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("box_subject_id", _boxConfig.Object.EnterpriseId));

Assert.AreEqual(accessToken, "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl");
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public async Task GetUserToken_ValidSession()
{
// Arrange
var userId = "22222";
IBoxRequest boxRequest = null;
_handler.Setup(h => h.ExecuteAsync<OAuthSession>(It.IsAny<BoxRequest>()))
.Returns(Task<IBoxResponse<OAuthSession>>.Factory.StartNew(() => new BoxResponse<OAuthSession>()
{
Status = ResponseStatus.Success,
ContentString = "{\"access_token\":\"T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl\",\"expires_in\":3600,\"restricted_to\":[],\"token_type\":\"bearer\"}"
}))
.Callback<IBoxRequest>(r => boxRequest = r);

// Act
var accessToken = await _ccgAuth.UserTokenAsync(userId);
Assert.AreEqual("https://api.box.com/oauth2/token", boxRequest.AbsoluteUri.AbsoluteUri);
Assert.AreEqual(RequestMethod.Post, boxRequest.Method);
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("grant_type", "client_credentials"));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("client_id", _boxConfig.Object.ClientId));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("client_secret", _boxConfig.Object.ClientSecret));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("box_subject_type", "user"));
Assert.IsTrue(boxRequest.PayloadParameters.ContainsKeyValue("box_subject_id", userId));

// Assert
Assert.AreEqual(accessToken, "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl");
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public void UserClient_ShouldReturnUserClientWithSession()
{
// Act
var userClient = _ccgAuth.UserClient("T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl", "22222");

// Assert
Assert.IsInstanceOfType(userClient, typeof(BoxClient));
Assert.IsInstanceOfType(userClient.Auth, typeof(CCGAuthRepository));
Assert.IsNotNull(userClient.Auth.Session);
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public void AdminClient_ShouldReturnAdminClientWithSession()
{
// Act
var adminClient = _ccgAuth.AdminClient("T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl", "22222", true);

// Assert
Assert.IsInstanceOfType(adminClient, typeof(BoxClient));
Assert.IsInstanceOfType(adminClient.Auth, typeof(CCGAuthRepository));
Assert.IsNotNull(adminClient.Auth.Session);
}
}
}
102 changes: 102 additions & 0 deletions Box.V2.Test/CCGAuthRepositoryTest.cs
@@ -0,0 +1,102 @@
using System;
using System.Threading.Tasks;
using Box.V2.Auth;
using Box.V2.CCGAuth;
using Box.V2.Config;
using Box.V2.Request;
using Box.V2.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;


namespace Box.V2.Test
{
[TestClass]
public class CCGAuthRepositoryTest : BoxResourceManagerTest
{
private readonly CCGAuthRepository _userAuthRepository;
private readonly CCGAuthRepository _adminAuthRepository;
private readonly Mock<IBoxConfig> _boxConfig;
private readonly Mock<IRequestHandler> _handler;
private readonly IBoxService _service;
private readonly BoxCCGAuth _ccgAuth;

private readonly string _userId = "22222";

public CCGAuthRepositoryTest()
{
// Initial Setup
_handler = new Mock<IRequestHandler>();
_service = new BoxService(_handler.Object);
_boxConfig = new Mock<IBoxConfig>();
_boxConfig.SetupGet(x => x.EnterpriseId).Returns("12345");
_boxConfig.SetupGet(x => x.ClientId).Returns("123");
_boxConfig.SetupGet(x => x.ClientSecret).Returns("SECRET");
_boxConfig.SetupGet(x => x.BoxApiHostUri).Returns(new Uri(Constants.BoxApiHostUriString));
_boxConfig.SetupGet(x => x.BoxAuthTokenApiUri).Returns(new Uri(Constants.BoxAuthTokenApiUriString));
_ccgAuth = new BoxCCGAuth(_boxConfig.Object, _service);
_userAuthRepository = new CCGAuthRepository(null, _ccgAuth, _userId);
_adminAuthRepository = new CCGAuthRepository(null, _ccgAuth, _userId);
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public async Task RefreshAccessTokenAsync_ForUser_ReturnsUserSession()
{
// Arrange
_handler.Setup(h => h.ExecuteAsync<OAuthSession>(It.IsAny<BoxRequest>()))
.Returns(Task<IBoxResponse<OAuthSession>>.Factory.StartNew(() => new BoxResponse<OAuthSession>()
{
Status = ResponseStatus.Success,
ContentString = "{\"access_token\":\"T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl\",\"expires_in\":3600,\"restricted_to\":[],\"token_type\":\"bearer\"}"
}));

// Act
var session = await _userAuthRepository.RefreshAccessTokenAsync(null);

// Assert
Assert.AreEqual(session.AccessToken, "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl");
Assert.AreEqual(session.TokenType, "bearer");
Assert.AreEqual(session.RefreshToken, null);
Assert.AreEqual(session.ExpiresIn, 3600);
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public async Task RefreshAccessTokenAsync_ForAdmin_ReturnsAdminSession()
{
// Arrange
_handler.Setup(h => h.ExecuteAsync<OAuthSession>(It.IsAny<BoxRequest>()))
.Returns(Task<IBoxResponse<OAuthSession>>.Factory.StartNew(() => new BoxResponse<OAuthSession>()
{
Status = ResponseStatus.Success,
ContentString = "{\"access_token\":\"T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl\",\"expires_in\":3600,\"restricted_to\":[],\"token_type\":\"bearer\"}"
}));

// Act
var session = await _adminAuthRepository.RefreshAccessTokenAsync(null);

// Assert
Assert.AreEqual(session.AccessToken, "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl");
Assert.AreEqual(session.TokenType, "bearer");
Assert.AreEqual(session.RefreshToken, null);
Assert.AreEqual(session.ExpiresIn, 3600);
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public void LogoutAsync_ThrowsException()
{
// Act & Assert
Assert.ThrowsExceptionAsync<NotImplementedException>(() => _adminAuthRepository.LogoutAsync());
}

[TestMethod]
[TestCategory("CI-UNIT-TEST")]
public void AuthenticateAsync_ThrowsException()
{
// Act & Assert
Assert.ThrowsExceptionAsync<NotImplementedException>(() => _adminAuthRepository.AuthenticateAsync(null));
}
}
}
14 changes: 14 additions & 0 deletions Box.V2.Test/Extensions/DictionaryExtensions.cs
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;

namespace Box.V2.Test.Extensions
{
public static class DictionaryExtensions
{
public static bool ContainsKeyValue<T>(this Dictionary<T, T> dictionary,
T expectedKey, T expectedValue) where T : IEquatable<T>
{
return dictionary.TryGetValue(expectedKey, out T actualValue) && EqualityComparer<T>.Default.Equals(actualValue, expectedValue);
}
}
}
4 changes: 3 additions & 1 deletion Box.V2/Box.V2.csproj
Expand Up @@ -33,7 +33,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'SignedRelease|AnyCPU' ">
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'SignedRelease|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
Expand Down Expand Up @@ -78,6 +78,8 @@
<Compile Include="Auth\Token\ActorTokenBuilder.cs" />
<Compile Include="Auth\Token\TokenExchange.cs" />
<Compile Include="BoxClient.cs" />
<Compile Include="CCGAuth\BoxCCGAuth.cs" />
<Compile Include="CCGAuth\CCGAuthRepository.cs" />
<Compile Include="Config\BoxConfigBuilder.cs" />
<Compile Include="IBoxClient.cs" />
<Compile Include="Converter\BoxZipConflictConverter.cs" />
Expand Down
114 changes: 114 additions & 0 deletions Box.V2/CCGAuth/BoxCCGAuth.cs
@@ -0,0 +1,114 @@
using System.Threading.Tasks;
using Box.V2.Auth;
using Box.V2.Config;
using Box.V2.Converter;
using Box.V2.Extensions;
using Box.V2.Request;
using Box.V2.Services;

namespace Box.V2.CCGAuth
{
public class BoxCCGAuth
{
private readonly IBoxService _boxService;
private readonly IBoxConfig _boxConfig;

/// <summary>
/// Constructor for CCG authentication
/// </summary>
/// <param name="boxConfig">Config contains information about client id, client secret, enterprise id.</param>
/// <param name="boxService">Box service is used to perform GetToken requests</param>
public BoxCCGAuth(IBoxConfig boxConfig, IBoxService boxService)
{
_boxConfig = boxConfig;
_boxService = boxService;
}

/// <summary>
/// Constructor for CCG authentication with default boxService
/// </summary>
/// <param name="boxConfig">Config contains information about client id, client secret, enterprise id.</param>
public BoxCCGAuth(IBoxConfig boxConfig) : this(boxConfig, new BoxService(new HttpRequestHandler(boxConfig.WebProxy, boxConfig.Timeout)))
{

}

/// <summary>
/// Create admin BoxClient using an admin access token
/// </summary>
/// <param name="adminToken">Admin access token</param>
/// <param name="asUser">The user ID to set as the 'As-User' header parameter; used to make calls in the context of a user using an admin token</param>
/// <param name="suppressNotifications">Whether or not to suppress both email and webhook notifications. Typically used for administrative API calls. Your application must have “Manage an Enterprise” scope, and the user making the API calls is a co-admin with the correct "Edit settings for your company" permission.</param>
/// <returns>BoxClient that uses CCG authentication</returns>
public IBoxClient AdminClient(string adminToken, string asUser = null, bool? suppressNotifications = null)
{
var adminSession = Session(adminToken);
var authRepo = new CCGAuthRepository(adminSession, this);
var adminClient = new BoxClient(_boxConfig, authRepo, asUser: asUser, suppressNotifications: suppressNotifications);

return adminClient;
}

/// <summary>
/// Create user BoxClient using a user access token
/// </summary>
/// <param name="userToken">User access token</param>
/// <param name="userId">Id of the user</param>
/// <returns>BoxClient that uses CCG authentication</returns>
public IBoxClient UserClient(string userToken, string userId)
{
var userSession = Session(userToken);
var authRepo = new CCGAuthRepository(userSession, this, userId);
var userClient = new BoxClient(_boxConfig, authRepo);

return userClient;
}

/// <summary>
/// Get admin token by posting data to auth url
/// </summary>
/// <returns>Admin token</returns>
public async Task<string> AdminTokenAsync()
{
return (await CCGAuthPostAsync(Constants.RequestParameters.EnterpriseSubType, _boxConfig.EnterpriseId).ConfigureAwait(false)).AccessToken;
}

/// <summary>
/// Once you have created an App User or Managed User, you can request a User Access Token via the App Auth feature, which will return the OAuth 2.0 access token for the specified User.
/// </summary>
/// <param name="userId">Id of the user</param>
/// <returns>User token</returns>
public async Task<string> UserTokenAsync(string userId)
{
return (await CCGAuthPostAsync(Constants.RequestParameters.UserSubType, userId).ConfigureAwait(false)).AccessToken;
}

private async Task<OAuthSession> CCGAuthPostAsync(string subType, string subId)
{
BoxRequest boxRequest = new BoxRequest(_boxConfig.BoxApiHostUri, Constants.AuthTokenEndpointString)
.Method(RequestMethod.Post)
.Payload(Constants.RequestParameters.GrantType, Constants.RequestParameters.ClientCredentials)
.Payload(Constants.RequestParameters.ClientId, _boxConfig.ClientId)
.Payload(Constants.RequestParameters.ClientSecret, _boxConfig.ClientSecret)
.Payload(Constants.RequestParameters.SubjectType, subType)
.Payload(Constants.RequestParameters.SubjectId, subId);

var converter = new BoxJsonConverter();
IBoxResponse<OAuthSession> boxResponse = await _boxService.ToResponseAsync<OAuthSession>(boxRequest).ConfigureAwait(false);
boxResponse.ParseResults(converter);

return boxResponse.ResponseObject;

}

/// <summary>
/// Create OAuth session from token
/// </summary>
/// <param name="token">Access token created by method UserToken, or AdminToken</param>
/// <returns>OAuth session</returns>
public OAuthSession Session(string token)
{
return new OAuthSession(token, null, Constants.AccessTokenExpirationTime, Constants.BearerTokenType);
}
}
}

0 comments on commit b8a64ca

Please sign in to comment.