Skip to content

Commit

Permalink
Added token utilities from LtiAdvantageTool.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmpease committed Jan 6, 2021
1 parent b6272de commit 52396e9
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/LtiAdvantage.IdentityModel/Client/AccessTokenUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityModel;
using IdentityModel.Client;
using LtiAdvantage.Utilities;
using Microsoft.IdentityModel.Tokens;

namespace LtiAdvantage.IdentityModel.Client
{
/// <summary>
/// Static utility to get an access token from the issuer.
/// </summary>
public static class AccessTokenUtil
{
private static readonly HttpClient HttpClient = new HttpClient();

/// <summary>
/// Get an access token from the issuer.
/// </summary>
/// <param name="issuer">The issuer.</param>
/// <param name="scopes">The scopes to request.</param>
/// <param name="clientId">The tool's client identifier.</param>
/// <param name="accessTokenUrl">The platform's access token url.</param>
/// <param name="privateKey">The tool's private key.</param>
/// <returns>The token response.</returns>
public static async Task<TokenResponse> GetAccessTokenAsync(string issuer, string[] scopes, string clientId, string accessTokenUrl, string privateKey)
{
if (issuer.IsMissing())
{
return new TokenResponse(new ArgumentNullException(nameof(issuer)));
}

if (scopes == null)
{
return new TokenResponse(new ArgumentNullException(nameof(scopes)));
}

if (clientId.IsMissing())
{
return new TokenResponse(new ArgumentNullException(nameof(clientId)));
}

if (accessTokenUrl.IsMissing())
{
return new TokenResponse(new ArgumentNullException(nameof(accessTokenUrl)));
}

if (privateKey.IsMissing())
{
return new TokenResponse(new ArgumentNullException(nameof(privateKey)));
}

// Use a signed JWT as client credentials.
var payload = new JwtPayload();
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Iss, clientId));
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Sub, clientId));
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Aud, accessTokenUrl));
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(DateTime.UtcNow).ToString()));
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Nbf,
EpochTime.GetIntDate(DateTime.UtcNow.AddSeconds(-5)).ToString(), ClaimValueTypes.Integer64));
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Exp,
EpochTime.GetIntDate(DateTime.UtcNow.AddMinutes(5)).ToString(), ClaimValueTypes.Integer64));
payload.AddClaim(new Claim(JwtRegisteredClaimNames.Jti, CryptoRandom.CreateRandomKeyString(32)));

var handler = new JwtSecurityTokenHandler();
var credentials = PemHelper.SigningCredentialsFromPemString(privateKey);
var jwt = handler.WriteToken(new JwtSecurityToken(new JwtHeader(credentials), payload));

return await HttpClient.RequestClientCredentialsTokenWithJwtAsync(
new JwtClientCredentialsTokenRequest
{
Address = accessTokenUrl,
ClientId = clientId,
Jwt = jwt,
Scope = string.Join(" ", scopes)
});
}
}
}
127 changes: 127 additions & 0 deletions src/LtiAdvantage.IdentityModel/Client/PemHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.IO;
using System.Security.Cryptography;
using IdentityModel;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;

namespace LtiAdvantage.IdentityModel.Client
{
/// <summary>
/// Helper utilities to read PEM formatted keys.
/// <remarks>
/// From https://dejanstojanovic.net/aspnet/2018/june/loading-rsa-key-pair-from-pem-files-in-net-core-with-c/
/// </remarks>
/// </summary>
public static class PemHelper
{
/// <summary>
/// A private/public key pair as PEM formatted strings.
/// </summary>
public class RsaKeyPair
{
public RsaKeyPair()
{
KeyId = CryptoRandom.CreateRandomKeyString(8);
}

/// <summary>
/// The KeyId for this key pair.
/// </summary>
public string KeyId { get; set; }

/// <summary>
/// The private key.
/// </summary>
public string PrivateKey { get; set; }

/// <summary>
/// The public key.
/// </summary>
public string PublicKey { get; set; }
}

/// <summary>
/// Create a new private/public key pair as PEM formatted strings.
/// </summary>
/// <returns>An <see cref="RsaKeyPair"/>.</returns>
public static RsaKeyPair GenerateRsaKeyPair()
{
var rsaGenerator = new RsaKeyPairGenerator();
rsaGenerator.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
var keyPair = rsaGenerator.GenerateKeyPair();

var rsaKeyPair = new RsaKeyPair();

using (var privateKeyTextWriter = new StringWriter())
{
var pemWriter = new PemWriter(privateKeyTextWriter);
pemWriter.WriteObject(keyPair.Private);
pemWriter.Writer.Flush();

rsaKeyPair.PrivateKey = privateKeyTextWriter.ToString();
}

using (var publicKeyTextWriter = new StringWriter())
{
var pemWriter = new PemWriter(publicKeyTextWriter);
pemWriter.WriteObject(keyPair.Public);
pemWriter.Writer.Flush();

rsaKeyPair.PublicKey = publicKeyTextWriter.ToString();
}

return rsaKeyPair;
}

/// <summary>
/// Converts a private key in PEM format into an <see cref="RsaSecurityKey"/>.
/// </summary>
/// <param name="privateKey">The private key.</param>
/// <returns>The private key as an <see cref="RsaSecurityKey"/>.</returns>
public static SigningCredentials SigningCredentialsFromPemString(string privateKey)
{
using (var keyTextReader = new StringReader(privateKey))
{
var cipherKeyPair = (AsymmetricCipherKeyPair)new PemReader(keyTextReader).ReadObject();

var keyParameters = (RsaPrivateCrtKeyParameters)cipherKeyPair.Private;
var parameters = new RSAParameters
{
Modulus = keyParameters.Modulus.ToByteArrayUnsigned(),
P = keyParameters.P.ToByteArrayUnsigned(),
Q = keyParameters.Q.ToByteArrayUnsigned(),
DP = keyParameters.DP.ToByteArrayUnsigned(),
DQ = keyParameters.DQ.ToByteArrayUnsigned(),
InverseQ = keyParameters.QInv.ToByteArrayUnsigned(),
D = keyParameters.Exponent.ToByteArrayUnsigned(),
Exponent = keyParameters.PublicExponent.ToByteArrayUnsigned()
};
var key = new RsaSecurityKey(parameters);
return new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
}
}

/// <summary>
/// Converts a public key in PEM format into an <see cref="RsaSecurityKey"/>.
/// </summary>
/// <param name="publicKey">The public key.</param>
/// <returns>The public key as an <see cref="RsaSecurityKey"/>.</returns>
public static RsaSecurityKey PublicKeyFromPemString(string publicKey)
{
using (var keyTextReader = new StringReader(publicKey))
{
var keyParameters = (RsaKeyParameters)new PemReader(keyTextReader).ReadObject();
var parameters = new RSAParameters
{
Modulus = keyParameters.Modulus.ToByteArrayUnsigned(),
Exponent = keyParameters.Exponent.ToByteArrayUnsigned()
};
return new RsaSecurityKey(parameters);
}
}
}
}
46 changes: 46 additions & 0 deletions src/LtiAdvantage.IdentityModel/Client/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Diagnostics;

namespace LtiAdvantage.Utilities
{
/// <summary>
/// Local version of Identity Server 4 internal static class StringExtensions
/// https://github.com/IdentityServer/IdentityServer4/blob/master/src/Extensions/StringsExtensions.cs
/// </summary>
public static class StringExtensions
{
[DebuggerStepThrough]
public static string EnsureTrailingSlash(this string url)
{
if (!url.EndsWith("/"))
{
return url + "/";
}

return url;
}

/// <summary>
/// Returns "[Not Set]" or replacement if string is missing.
/// </summary>
/// <param name="value">The string.</param>
/// <param name="replacement">The replacement (defaults to "[Not Set]").</param>
/// <returns>A string.</returns>
[DebuggerStepThrough]
public static string IfMissingThen(this string value, string replacement = "[Not Set]")
{
return string.IsNullOrWhiteSpace(value) ? replacement : value;
}

[DebuggerStepThrough]
public static bool IsMissing(this string value)
{
return string.IsNullOrWhiteSpace(value);
}

[DebuggerStepThrough]
public static bool IsPresent(this string value)
{
return !string.IsNullOrWhiteSpace(value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.3" />
<PackageReference Include="IdentityModel" Version="3.10.5" />
<PackageReference Include="MinVer" Version="2.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.3.0" />
</ItemGroup>

</Project>

0 comments on commit 52396e9

Please sign in to comment.