|
| 1 | +using Microsoft.IdentityModel.Protocols.OpenIdConnect; |
| 2 | +using Microsoft.IdentityModel.Protocols; |
| 3 | +using Microsoft.IdentityModel.Tokens; |
| 4 | +using System; |
| 5 | +using System.Collections.Generic; |
| 6 | +using System.Net.Http; |
| 7 | +using System.Security.Cryptography; |
| 8 | +using System.Threading.Tasks; |
| 9 | +using AuthFlow; |
| 10 | +using System.Text.Json; |
| 11 | + |
| 12 | +namespace ConsolePerInstanceAssertionES256; |
| 13 | + |
| 14 | +/// <summary> |
| 15 | +/// Creates a new key for the application client assertion. |
| 16 | +/// The public key is exchanged with the IDP and connected with a session. |
| 17 | +/// The application should validate more identity data like an email, sms and connect this to the session. |
| 18 | +/// </summary> |
| 19 | +public class KeySessionService |
| 20 | +{ |
| 21 | + /// <summary> |
| 22 | + /// One signing key per application instance |
| 23 | + /// </summary> |
| 24 | + private static (string? AuthSession, SigningCredentials? SigningCredentials) _inMemoryCache = (null, null); |
| 25 | + |
| 26 | + private readonly AuthFlowConfiguration _authFlowConfiguration = new AuthFlowConfiguration |
| 27 | + { |
| 28 | + ClientId = "cid-fp-device", |
| 29 | + TokenMetadataAddress = "https://localhost:5101/.well-known/openid-configuration", |
| 30 | + TokenAuthority = "https://localhost:5101" |
| 31 | + }; |
| 32 | + |
| 33 | + public async Task<(string? AuthSession, SigningCredentials? SigningCredentials)> CreateGetSessionAsync() |
| 34 | + { |
| 35 | + if (_inMemoryCache.AuthSession != null) |
| 36 | + { |
| 37 | + return _inMemoryCache; |
| 38 | + } |
| 39 | + |
| 40 | + var ecdsa = ECDsa.Create(); |
| 41 | + ecdsa.KeySize = 256; |
| 42 | + var ecdsaCertificateKey = new ECDsaSecurityKey(ecdsa); |
| 43 | + var publicKeyPem = ecdsa.ExportSubjectPublicKeyInfoPem(); |
| 44 | + |
| 45 | + var httpClient = new HttpClient(); |
| 46 | + |
| 47 | + var nonce = RandomNumberGenerator.GetHexString(73); |
| 48 | + var state = RandomNumberGenerator.GetHexString(67); |
| 49 | + //client_id = cid_235saw4r4 |
| 50 | + //& grant_type = fp_register |
| 51 | + //& public_key =< public_key > |
| 52 | + //&state =< state > |
| 53 | + //&nonce =< nonce > |
| 54 | + var formData = new List<KeyValuePair<string, string>> |
| 55 | + { |
| 56 | + new KeyValuePair<string, string>("client_id", _authFlowConfiguration.ClientId), |
| 57 | + new KeyValuePair<string, string>("grant_type", OAuthConsts.GRANT_TYPE), |
| 58 | + new KeyValuePair<string, string>("public_key", publicKeyPem), |
| 59 | + new KeyValuePair<string, string>("alg", "ES256"), |
| 60 | + new KeyValuePair<string, string>("state", state), |
| 61 | + new KeyValuePair<string, string>("nonce", nonce) |
| 62 | + }; |
| 63 | + |
| 64 | + // Encodes the key-value pairs for the ContentType 'application/x-www-form-urlencoded' |
| 65 | + HttpContent content = new FormUrlEncodedContent(formData); |
| 66 | + var response = await httpClient.PostAsync("https://localhost:5101/api/DeviceRegistration", content); |
| 67 | + |
| 68 | + if (response.IsSuccessStatusCode) |
| 69 | + { |
| 70 | + var signingCredentials = new SigningCredentials(ecdsaCertificateKey, "ES256"); |
| 71 | + var responseResult = await response.Content.ReadAsStringAsync(); |
| 72 | + var deviceRegistrationResponse = JsonSerializer.Deserialize<DeviceRegistrationResponse>(responseResult); |
| 73 | + |
| 74 | + if (deviceRegistrationResponse == null) |
| 75 | + { |
| 76 | + throw new Exception("no response"); |
| 77 | + } |
| 78 | + // TODO |
| 79 | + // Validate state |
| 80 | + // Validate JWT signing credential |
| 81 | + // Validate nbf, exp, iat |
| 82 | + // Validate nonce |
| 83 | + // Validate aud (clientId) |
| 84 | + // Validate iss |
| 85 | + // Validate "typ": "fp+jwt" |
| 86 | + |
| 87 | + var (Valid, Reason, Error) = ValidateTokenResponsePayload |
| 88 | + .IsValid(deviceRegistrationResponse, _authFlowConfiguration, state); |
| 89 | + |
| 90 | + if (!Valid) |
| 91 | + { |
| 92 | + Console.WriteLine($"UnauthorizedValidationParametersFailed {Reason} {Error}"); |
| 93 | + throw new ArgumentNullException("auth_session", "UnauthorizedValidationParametersFailed"); |
| 94 | + } |
| 95 | + |
| 96 | + // get well known endpoints and validate access token sent in the assertion |
| 97 | + var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>( |
| 98 | + _authFlowConfiguration.TokenMetadataAddress, |
| 99 | + new OpenIdConnectConfigurationRetriever()); |
| 100 | + |
| 101 | + var wellKnownEndpoints = await configurationManager.GetConfigurationAsync(); |
| 102 | + |
| 103 | + var deviceTokenValidationResult = await ValidateTokenResponsePayload.ValidateTokenAndSignature( |
| 104 | + deviceRegistrationResponse.FpToken, _authFlowConfiguration, wellKnownEndpoints.SigningKeys); |
| 105 | + |
| 106 | + if (!deviceTokenValidationResult.Valid) |
| 107 | + { |
| 108 | + Console.WriteLine($"UnauthorizedValidationTokenAndSignatureFailed {Reason} {Error}"); |
| 109 | + throw new ArgumentNullException("auth_session", "UnauthorizedValidationTokenAndSignatureFailed"); |
| 110 | + } |
| 111 | + |
| 112 | + var nonceInResponse = ValidateTokenResponsePayload.GetNonce(deviceTokenValidationResult.ClaimsIdentity!); |
| 113 | + if (nonceInResponse != nonce) |
| 114 | + { |
| 115 | + Console.WriteLine("Nonce validation failed"); |
| 116 | + throw new ArgumentNullException("auth_session", "Nonce validation failed"); |
| 117 | + } |
| 118 | + |
| 119 | + var authSession = ValidateTokenResponsePayload.GetAuthSession(deviceTokenValidationResult.ClaimsIdentity!); |
| 120 | + _inMemoryCache = (authSession, signingCredentials); |
| 121 | + |
| 122 | + // TODO persist key in TPM and re-use |
| 123 | + |
| 124 | + return _inMemoryCache; |
| 125 | + } |
| 126 | + |
| 127 | + throw new ArgumentNullException("auth_session", "something went wrong"); |
| 128 | + } |
| 129 | +} |
0 commit comments