Last active
February 27, 2021 22:26
-
-
Save LasseSkogland/b65ee0df912fef2397e920cd507771a8 to your computer and use it in GitHub Desktop.
Implementation of the new DuoWeb SDK v4 (frameless) in C# (Should support Universal Prompt)
This file contains hidden or 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
| using System; | |
| using System.Collections.Generic; | |
| using System.Dynamic; | |
| using System.Linq; | |
| using System.Net.Http; | |
| using System.Security.Cryptography; | |
| using System.Text; | |
| using System.Text.Json; | |
| using System.Threading.Tasks; | |
| using System.Web; | |
| namespace Duo { | |
| public class Web { | |
| private readonly HttpClient Client; | |
| private readonly string ClientId; | |
| private readonly string SecretKey; | |
| private readonly string ApiHostame; | |
| public Web(string clientId, string secretKey, string apiHostname) { | |
| Client = new HttpClient(); | |
| ClientId = clientId; | |
| SecretKey = secretKey; | |
| ApiHostame = apiHostname; | |
| } | |
| public async Task<ExpandoObject> HealthCheck() { | |
| var url = $"https://{ApiHostame}/oauth/v1/health_check"; | |
| var jwt = EncodeJwt(CreateAssertion(url), "HS512", SecretKey); | |
| var formContent = new FormUrlEncodedContent(new Dictionary<string, string>() { | |
| { "client_id", ClientId }, | |
| { "client_assertion", jwt} | |
| }); | |
| var res = await Client.PostAsync(url, formContent); | |
| var jsonResponse = await res.Content.ReadAsStringAsync(); | |
| return JsonSerializer.Deserialize<ExpandoObject>(jsonResponse); | |
| } | |
| public string GetAuthorizeUrl(string username, string redirectUri) { | |
| var url = $"https://{ApiHostame}/oauth/v1/authorize"; | |
| var jwt = EncodeJwt(new { | |
| response_type = "code", | |
| scope = "openid", | |
| exp = ToUnixTimestamp(DateTime.UtcNow.AddMinutes(5)), | |
| client_id = ClientId, | |
| redirect_uri = redirectUri, | |
| state = GetRandomAlphanumericString(40), | |
| duo_uname = username | |
| }, "HS512", SecretKey); | |
| return $"{url}?response_type=code&client_id={ClientId}&request={HttpUtility.UrlEncode(jwt)}&redirect_uri={HttpUtility.UrlEncode(redirectUri)}&scope=openid"; | |
| } | |
| public async Task<ExpandoObject> Token(string code, string redirectUri) { | |
| var url = $"https://{ApiHostame}/oauth/v1/token"; | |
| var jwt = EncodeJwt(CreateAssertion(url), "HS512", SecretKey); | |
| var formContent = new FormUrlEncodedContent(new Dictionary<string, string>() { | |
| { "grant_type", "authorization_code" }, | |
| { "code", code }, | |
| { "redirect_uri", redirectUri }, | |
| { "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" }, | |
| { "client_assertion", jwt } | |
| }); | |
| var res = await Client.PostAsync(url, formContent); | |
| var jsonResponse = await res.Content.ReadAsStringAsync(); | |
| return JsonSerializer.Deserialize<ExpandoObject>(jsonResponse); | |
| } | |
| #region Utility functions | |
| private static readonly Random _random = new Random(); | |
| private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); | |
| private static string ToBase64String(string input) => Convert.ToBase64String(Encoding.UTF8.GetBytes(input)); | |
| private static int ToUnixTimestamp(DateTime dateTime) => (int)Math.Floor((dateTime.ToUniversalTime() - epoch).TotalSeconds); | |
| private static string GetRandomAlphanumericString(int length) { | |
| const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; | |
| return new string(Enumerable.Repeat(chars, length) | |
| .Select(s => s[_random.Next(s.Length)]).ToArray()); | |
| } | |
| private static dynamic CreateAssertion(string url) { | |
| var utcNow = ToUnixTimestamp(DateTime.UtcNow); | |
| return new { | |
| iss = ClientId, | |
| sub = ClientId, | |
| aud = url, | |
| exp = utcNow + 300, | |
| jti = GetRandomAlphanumericString(40), | |
| iat = utcNow | |
| }; | |
| } | |
| private static string Hmac512Sign(string skey, string data, string alg) { | |
| byte[] key_bytes = Encoding.UTF8.GetBytes(skey); | |
| HMAC hmac = alg switch { | |
| "HS512" => new HMACSHA512(key_bytes), | |
| "HS384" => new HMACSHA384(key_bytes), | |
| "HS256" => new HMACSHA256(key_bytes), | |
| _ => new HMACSHA1(), | |
| }; | |
| using (hmac) { | |
| byte[] data_bytes = Encoding.UTF8.GetBytes(data); | |
| hmac.ComputeHash(data_bytes); | |
| return Convert.ToBase64String(hmac.Hash); | |
| } | |
| } | |
| public static string EncodeJwt(dynamic payload, string alg, string secretKey) { | |
| var jsonHeader = JsonSerializer.Serialize(new { alg, type = "JWT" }); | |
| string jsonPayload = JsonSerializer.Serialize(payload); | |
| var body = ToBase64String(jsonHeader) + '.' + ToBase64String(jsonPayload); | |
| var signature = Hmac512Sign(secretKey, body, alg); | |
| return body + '.' + signature; | |
| } | |
| public static string[] DecodeJwt(string jwtToken) { | |
| string[] parts = jwtToken.Split('.'); | |
| for (int i = 0; i < parts.Length; i++) { | |
| var part = parts[i]; | |
| part = part.Replace('_', '/').Replace('-', '+'); | |
| switch (part.Length % 4) { | |
| case 2: part += "=="; break; | |
| case 3: part += "="; break; | |
| } | |
| if (i == 2) { | |
| parts[i] = part; | |
| } | |
| else { | |
| parts[i] = Encoding.UTF8.GetString(Convert.FromBase64String(part)); | |
| } | |
| } | |
| return parts; | |
| } | |
| #endregion | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment