diff --git a/APSToolkit/Auth.cs b/APSToolkit/Auth.cs index 894fc12..24c1f67 100644 --- a/APSToolkit/Auth.cs +++ b/APSToolkit/Auth.cs @@ -2,9 +2,12 @@ using System.Diagnostics; using System.Net; +using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; using Autodesk.Forge; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace APSToolkit; @@ -14,13 +17,23 @@ public class Auth private string? ClientSecret { get; set; } private Token Token { get; set; } - public Auth(string? clientId, string? clientSecret) + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public Auth(string? clientId=null, string? clientSecret=null) { this.ClientId = clientId; this.ClientSecret = clientSecret; this.Token = new Token(); } + /// + /// Initializes a new instance of the class. + /// If the client ID and client secret are not provided, the constructor retrieves them from the environment variables. + /// public Auth() { this.ClientId = Environment.GetEnvironmentVariable("APS_CLIENT_ID",EnvironmentVariableTarget.User); @@ -53,7 +66,12 @@ public string GetClientId() { throw new Exception("Missing APS_CLIENT_ID environment variable."); } - + Scope[] scope = new Scope[] + { + Scope.DataRead, Scope.DataWrite, Scope.DataCreate, Scope.DataSearch, Scope.BucketCreate, + Scope.BucketRead, Scope.CodeAll, + Scope.BucketUpdate, Scope.BucketDelete + }; return clientId; } @@ -171,7 +189,6 @@ public async Task Get3LeggedToken(string? callbackUrl = null, string? sco Environment.SetEnvironmentVariable("APS_REFRESH_TOKEN", Token.RefreshToken, EnvironmentVariableTarget.User); return Token; } - private async Task HandleCallback(string callbackUrl, string? code) { var tokenUrl = "https://developer.api.autodesk.com/authentication/v2/token"; @@ -204,6 +221,119 @@ private void OpenDefaultBrowser(string url) Console.WriteLine($"Error opening default browser: {ex.Message}"); } } + public async Task Get3LeggedTokenPkce(string? callbackUrl = null, string? scopes = null) + { + if (string.IsNullOrEmpty(scopes)) + { + scopes = "data:read"; + } + + if (string.IsNullOrEmpty(callbackUrl)) + { + callbackUrl = "http://localhost:8080/api/auth/callback"; + } + string codeVerifier = RandomString(64); + string codeChallenge = GenerateCodeChallenge(codeVerifier); + string authUrl = $"https://developer.api.autodesk.com/authentication/v2/authorize?response_type=code&client_id={ClientId}&redirect_uri={callbackUrl}&scope={scopes}&prompt=login&code_challenge={codeChallenge}&code_challenge_method=S256"; + OpenDefaultBrowser(authUrl); + // get prefix from callbackUrl just get http://localhost:8080/api/auth/ from http://localhost:8080/api/auth/callback + string prefix = callbackUrl.Substring(0, callbackUrl.LastIndexOf('/'))+"/"; + var listenerTask = CallListener(prefix, codeVerifier, callbackUrl, scopes); + await listenerTask; + Environment.SetEnvironmentVariable("APS_REFRESH_TOKEN", Token.RefreshToken, EnvironmentVariableTarget.User); + return Token; + } + private async Task CallListener(string prefix,string codeVerifier,string callbackUrl,string scopes) + { + if (!HttpListener.IsSupported) + { + throw new NotSupportedException("HttpListener is not supported in this context!"); + } + if (prefix == null || prefix.Length == 0) + throw new ArgumentException("prefixes"); + HttpListener listener = new HttpListener(); + listener.Prefixes.Add(prefix); + listener.Start(); + HttpListenerContext context = await listener.GetContextAsync(); + HttpListenerRequest request = context.Request; + // Obtain a response object. + HttpListenerResponse response = context.Response; + + try + { + string? authCode = request.Url?.Query.ToString().Split('=')[1]; + await GetPkceToken(authCode,codeVerifier,callbackUrl,scopes); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + + // Construct a response. + string responseString = " Authentication successful. You can close this window now."; + byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString); + // Get a response stream and write the response to it. + response.ContentLength64 = buffer.Length; + System.IO.Stream output = response.OutputStream; + output.Write(buffer, 0, buffer.Length); + // You must close the output stream. + output.Close(); + listener.Stop(); + } + private async Task GetPkceToken(string? authCode,string codeVerifier,string callbackUrl,string scopes) + { + try + { + var client = new HttpClient(); + var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri("https://developer.api.autodesk.com/authentication/v2/token"), + Content = new FormUrlEncodedContent(new Dictionary + { + { "client_id", ClientId }, + { "code_verifier", codeVerifier }, + { "code", authCode}, + { "scope", scopes }, + { "grant_type", "authorization_code" }, + { "redirect_uri", callbackUrl } + }), + }; + + using (var response = await client.SendAsync(request)) + { + response.EnsureSuccessStatusCode(); + string bodystring = await response.Content.ReadAsStringAsync(); + JObject bodyjson = JObject.Parse(bodystring); + this.Token.AccessToken = bodyjson["access_token"]!.Value(); + this.Token.TokenType = bodyjson["token_type"]!.Value(); + this.Token.ExpiresIn = bodyjson["expires_in"]!.Value(); + this.Token.RefreshToken = bodyjson["refresh_token"]!.Value(); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + private string RandomString(int length) + { + Random random = new Random(); + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + + private string GenerateCodeChallenge(string codeVerifier) + { + var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); + var b64Hash = Convert.ToBase64String(hash); + var code = Regex.Replace(b64Hash, "\\+", "-"); + code = Regex.Replace(code, "\\/", "_"); + code = Regex.Replace(code, "=+$", ""); + return code; + } /// /// Refreshes a 3-legged access token from the Autodesk Forge API using the refresh token grant type. diff --git a/APSToolkitUnit/AuthTest.cs b/APSToolkitUnit/AuthTest.cs index 78af2a6..d1e5e90 100644 --- a/APSToolkitUnit/AuthTest.cs +++ b/APSToolkitUnit/AuthTest.cs @@ -26,9 +26,19 @@ public void TestAuthentication2Leg() public void TestAuthentication3Leg() { Token = Auth.Get3LeggedToken().Result; + Assert.IsNotNull(Token.AccessToken); Assert.IsNotEmpty(Token.AccessToken); } - + [Test] + public Task TestAuthentication3LegPkce() + { + string clientId = Environment.GetEnvironmentVariable("APS_CLIENT_ID_PKCE", EnvironmentVariableTarget.User); + Auth = new Auth(clientId); + Token = Auth.Get3LeggedTokenPkce().Result; + Assert.IsNotNull(Token.AccessToken); + Assert.IsNotEmpty(Token.RefreshToken); + return Task.CompletedTask; + } [Test] public Task TestRefresh3LegToken() {