-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added token utilities from LtiAdvantageTool.
- Loading branch information
Showing
4 changed files
with
257 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters