diff --git a/Acumatica REST API Console Application/OAuthHybridExample.cs b/Acumatica REST API Console Application/OAuthHybridExample.cs new file mode 100644 index 00000000..823fc74d --- /dev/null +++ b/Acumatica REST API Console Application/OAuthHybridExample.cs @@ -0,0 +1,136 @@ +using Acumatica.Default_24_200_001.Model; +using Acumatica.RESTClient.AuthApi.Model; +using Acumatica.RESTClient.Client; +using Acumatica.RESTClient.Loggers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using static Acumatica.RESTClient.AuthApi.AuthApiExtensions; +using static Acumatica.RESTClient.ContractBasedApi.ApiClientExtensions; +using Task = System.Threading.Tasks.Task; + +namespace AcumaticaRestApiExample +{ + internal class OAuthHybridExample + { + public static void Example(string siteURL, string clientSecret, string clientID, string redirectUrl) + { + var client = new ApiClient(siteURL, + requestInterceptor: FileRequestLogger.LogRequest + // ,responseInterceptor: RequestLogger.LogResponse + , ignoreSslErrors: true // this is here to allow testing with self-signed certificates + ); + bool usePost = true; //This is here to read the the Authorization data in backend + var url = client.Authorize( + clientID, + clientSecret, + redirectUrl, + OAuthScope.API | OAuthScope.OfflineAccess | OAuthScope.OpenID, + ResponseType.IdToken,// | ResponseType.Token, + usePost, + Guid.NewGuid().ToString() + ); + OpenUrl(url); + + var code = ReadCodeFromRedirectURL(redirectUrl, usePost, out Token token); + + client.ReceiveAccessTokenAuthCode( + clientID, + clientSecret, + redirectUrl, + code); + + foreach (var account in client.GetList(top: 5)) + { + Console.WriteLine(account.Description.Value); + } + client.TryLogout(); + + foreach (var soorder in client.GetList(top: 5)) + { + Console.WriteLine(soorder.Description.Value); + } + client.TryLogout(); + + } + + private static string ReadCodeFromRedirectURL(string url, bool usePost, out Token token) + { + HttpListener listener = new HttpListener(); + listener.Prefixes.Add(url); + token = new Token(); + try + { + listener.Start(); + string purecode = string.Empty; + + HttpListenerContext context = listener.GetContext(); + HttpListenerRequest request = context.Request; + if (usePost) + { + using (System.IO.Stream body = context.Request.InputStream) // here we have data + { + using (var reader = new System.IO.StreamReader(body, context.Request.ContentEncoding)) + { + var values = HttpUtility.ParseQueryString(reader.ReadToEnd()); + Console.WriteLine(); + token.Access_token = values.Get("access_token"); + token.Expires_in = values.Get("expires_in"); + token.Token_type = values.Get("token_type"); + token.Scope = values.Get("scope"); + purecode = values.Get("code") ?? string.Empty; + } + } + } + else + { + var rawUrl = request.RawUrl; + const string codeParametrIdentifier = "?code="; + var codewithgarbage = rawUrl.Substring(rawUrl.IndexOf(codeParametrIdentifier) + codeParametrIdentifier.Length); + purecode = codewithgarbage.Substring(0, codewithgarbage.IndexOf("&")); + } + return purecode; + } + finally + { + listener.Stop(); + } + } + + + private static void OpenUrl(string url) + { + try + { + Process.Start(url); + } + catch + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw; + } + } + } + } +} diff --git a/Acumatica REST API Console Application/Program.cs b/Acumatica REST API Console Application/Program.cs index a4a858bd..b7c1b1b4 100644 --- a/Acumatica REST API Console Application/Program.cs +++ b/Acumatica REST API Console Application/Program.cs @@ -20,13 +20,17 @@ class Program private const string ClientSecretAC = "ZJ7sGGDZWOJyJzSpbRjrpg"; private const string ClientIDAC = "9737F884-8405-42FB-7303-7F1DA7BE8CA7@Company"; #endregion + # region Hybrid flow + private const string ClientSecretHybrid = "KzK82VVdqggy4PHaOHMTNw"; + private const string ClientIDHybrid = "2DC9435C-A596-959E-3E38-8EB84725F089@Company"; + #endregion const string RedirectUrl = "https://localhost/test/"; static async Task Main(string[] args) - { - Console.WriteLine("Update example"); - Console.WriteLine("----------------------------------------"); - RESTExample.UpdateBill(SiteURL, Username, Password, Tenant, Branch, Locale); + { + Console.WriteLine("Update example"); + Console.WriteLine("----------------------------------------"); + RESTExample.UpdateBill(SiteURL, Username, Password, Tenant, Branch, Locale); Console.WriteLine("Report example"); @@ -80,6 +84,12 @@ static async Task Main(string[] args) OAuthAuthCodeExample.Example(SiteURL, ClientSecretAC, ClientIDAC, RedirectUrl); Console.WriteLine("Ready to continue..."); Console.ReadLine(); + + Console.WriteLine("OAuth 2.0 (Hybrid flow)"); + Console.WriteLine("----------------------------------------"); + OAuthHybridExample.Example(SiteURL, ClientSecretHybrid, ClientIDHybrid, RedirectUrl); + Console.WriteLine("Ready to continue..."); + Console.ReadLine(); //await TestPerformanceAsync(); } diff --git a/Acumatica.RESTClient/AuthApi/AuthApi.cs b/Acumatica.RESTClient/AuthApi/AuthApi.cs index e5c42c7d..9a626437 100644 --- a/Acumatica.RESTClient/AuthApi/AuthApi.cs +++ b/Acumatica.RESTClient/AuthApi/AuthApi.cs @@ -1,3 +1,6 @@ +using Acumatica.RESTClient.Api; +using Acumatica.RESTClient.AuthApi.Model; +using Acumatica.RESTClient.Client; using System; using System.Collections.Generic; using System.Linq; @@ -5,11 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; - -using Acumatica.RESTClient.Api; -using Acumatica.RESTClient.AuthApi.Model; -using Acumatica.RESTClient.Client; - using static Acumatica.RESTClient.Auxiliary.ApiClientHelpers; @@ -117,9 +115,12 @@ public async static Task ReceiveAccessTokenAsync( /// /// /// - public static string Authorize(this ApiClient client, string clientID, string clientSecret, string redirectUrl, OAuthScope scope) + /// + /// + /// + public static string Authorize(this ApiClient client, string clientID, string clientSecret, string redirectUrl, OAuthScope scope, ResponseType? responseType = null, bool usePost = false, string nonce = "") { - return AuthorizeAsync(client, clientID, clientSecret, redirectUrl, scope).Result; + return AuthorizeAsync(client, clientID, clientSecret, redirectUrl, scope, responseType: responseType, usePost: usePost, nonce: nonce).Result; } /// /// @@ -130,20 +131,28 @@ public static string Authorize(this ApiClient client, string clientID, string cl /// /// /// + /// + /// + /// public async static Task AuthorizeAsync( this ApiClient client, string clientID, string clientSecret, string redirectUrl, OAuthScope scope, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + ResponseType? responseType = null, + bool usePost = false, + string nonce = "") { List> queryParams = new List> { - new KeyValuePair("response_type", "code"), + new KeyValuePair("response_type", PrepareResponseType(responseType)), new KeyValuePair("client_id", clientID), new KeyValuePair("scope", PrepareScopeParameter(scope)), - new KeyValuePair("redirect_uri", redirectUrl) + new KeyValuePair("redirect_uri", redirectUrl), + new KeyValuePair("response_mode", usePost ? "form_post" : "fragment"), + new KeyValuePair("nonce", nonce) }; HttpResponseMessage response = await client.CallApiAsync( @@ -385,7 +394,30 @@ public enum OAuthScope None = 0, API = 1, OfflineAccess = 2, - ConcurrentAccess = 4 + ConcurrentAccess = 4, + OpenID = 8 + } + + [Flags] + public enum ResponseType + { + IdToken = 0, + Token = 1 + } + + private static string PrepareResponseType(ResponseType? responseType) + { + StringBuilder s = new StringBuilder(); + s.Append("code"); + if (responseType != null) + { + if (responseType.Value.HasFlag(ResponseType.IdToken)) + s.Append(" id_token"); + if (responseType.Value.HasFlag(ResponseType.Token)) + s.Append(" token"); + } + + return s.ToString().TrimEnd(' '); } private static string PrepareScopeParameter(OAuthScope scope) @@ -396,7 +428,9 @@ private static string PrepareScopeParameter(OAuthScope scope) if (scope.HasFlag(OAuthScope.OfflineAccess)) s.Append("offline_access "); if (scope.HasFlag(OAuthScope.ConcurrentAccess)) - s.Append("api:concurrent_access "); + s.Append("api:concurrent_access "); + if (scope.HasFlag(OAuthScope.OpenID)) + s.Append("openid "); return s.ToString().TrimEnd(' '); }