377 lines
16 KiB
C#
377 lines
16 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Net.Http;
|
|
using Newtonsoft.Json.Linq;
|
|
using Newtonsoft.Json;
|
|
using System.Net.Http.Formatting;
|
|
|
|
namespace qbridge.Controllers
|
|
{
|
|
[Route("[controller]")]
|
|
[ApiController]
|
|
[Produces("application/json")]
|
|
public class OAuthRedirectController : ControllerBase
|
|
{
|
|
public const string CLIENT_ID = "ABj70Wv5gDauFd9KgKFwuvpQjfzTwEgodEG8tnBbS8mSQhNrZJ";
|
|
public const string CLIENT_SECRET = "XUmJyvEcEuwQuyhARUAm0a8G3gzbEAeMiATCLyFZ";
|
|
public const string REDIRECT_URI = "https://localhost:5001/OAuthRedirect";
|
|
|
|
//used for discovery document
|
|
//https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.0
|
|
private readonly IHttpClientFactory _clientFactory;
|
|
public static JObject DiscoveryDoc { get; private set; }
|
|
|
|
/*
|
|
|
|
|
|
Discovery document
|
|
https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/openid-connect#discovery-document
|
|
Endpoints:
|
|
https://developer.api.intuit.com/.well-known/openid_sandbox_configuration
|
|
https://developer.api.intuit.com/.well-known/openid_configuration
|
|
|
|
QB Online developer account login creds for sandbox:
|
|
|
|
login: support@ayanova.com
|
|
pw: i8BREAKfast!
|
|
|
|
|
|
Development tokens for QBOI oAuth2 "AyaNova_QBOI_2"
|
|
|
|
https://developer.intuit.com/v2/ui#/app/appdetail/b7urd26wgx/b7urd26xgp/keys
|
|
|
|
ClientID
|
|
ABj70Wv5gDauFd9KgKFwuvpQjfzTwEgodEG8tnBbS8mSQhNrZJ
|
|
|
|
Client Secret
|
|
XUmJyvEcEuwQuyhARUAm0a8G3gzbEAeMiATCLyFZ
|
|
|
|
|
|
Sandbox:
|
|
https://c50.sandbox.qbo.intuit.com/app/homepage
|
|
sandbox company_us_1
|
|
sandbox-quickbooks.api.intuit.com
|
|
*/
|
|
public OAuthRedirectController(IHttpClientFactory clientFactory)
|
|
{
|
|
_clientFactory = clientFactory;
|
|
}
|
|
|
|
|
|
//bugbug: It's not prompting for the company I want like the playground does...what's up with that? Is it because I need to follow the OpenID method rather than the "web app" method??
|
|
//realmid is companyid in some examples but I don't get prompted for it unless I use the playground version
|
|
//maybe the example I followed assumes you already have the company ID when you make subsequent api calls
|
|
/*
|
|
|
|
Stuff from playground
|
|
Step 1 if I select only openID scope (no company prompt)
|
|
https://appcenter.intuit.com/connect/oauth2?client_id=ABj70Wv5gDauFd9KgKFwuvpQjfzTwEgodEG8tnBbS8mSQhNrZJ&scope=openid%20profile%20email%20phone%20address&redirect_uri=https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl&response_type=code&state=PlaygroundAuth
|
|
|
|
Step 1 if I select accounting only and not openid scope:
|
|
https://appcenter.intuit.com/connect/oauth2?client_id=ABj70Wv5gDauFd9KgKFwuvpQjfzTwEgodEG8tnBbS8mSQhNrZJ&scope=com.intuit.quickbooks.accounting&redirect_uri=https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl&response_type=code&state=PlaygroundAuth
|
|
|
|
*/
|
|
|
|
[HttpGet("Start/{qboid}")]
|
|
public async Task<IActionResult> GetAsync([FromRoute]string qboid)
|
|
{
|
|
|
|
if (string.IsNullOrWhiteSpace(qboid))
|
|
{
|
|
return BadRequest("QBOID value is required");
|
|
}
|
|
|
|
//GET THE DISCOVERY DOCUMENT
|
|
//Discovery document contains the actual current endpoints to use for various ops
|
|
await GetQBDiscoveryDocument();
|
|
if (DiscoveryDoc == null)
|
|
{
|
|
return Content($"<h1>Error - Unable to fetch Discovery document from QuickBooks Online</h1>Cannot proceed");
|
|
}
|
|
|
|
|
|
var AuthorizationEndpoint = DiscoveryDoc["authorization_endpoint"].Value<string>();
|
|
if (string.IsNullOrWhiteSpace(AuthorizationEndpoint))
|
|
{
|
|
return Content($"<h1>Error - Unable to find AuthorizationEndpoint value in Discovery document from QuickBooks Online</h1>Cannot proceed");
|
|
}
|
|
|
|
|
|
//GET AUTHORIZATION CODE AND REDIRECT
|
|
|
|
string url = string.Empty;
|
|
var queryParams = new Dictionary<string, string>()
|
|
{
|
|
{"client_id", CLIENT_ID },
|
|
{"scope", "com.intuit.quickbooks.accounting" },
|
|
{"redirect_uri",REDIRECT_URI },
|
|
{"response_type","code"},
|
|
{"state",qboid}
|
|
};
|
|
|
|
url = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(AuthorizationEndpoint, queryParams);
|
|
return Redirect(url);
|
|
|
|
//will ask for login creds and permission then will redirect back to the Get method below with:
|
|
//State: MyUniqueStateID, Code: AB11569366500tshFDRPEuR28l4vTjpXWuwFldE44rMng98Gn9
|
|
//This Code value is the "Authorization code" that is then used to get the access token
|
|
|
|
//which in turn is *then* used to actually get the access token
|
|
|
|
|
|
}
|
|
|
|
|
|
// Redirect endpoint
|
|
//Step 4 here: https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/openid-connect
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetAsync([FromQuery]string state, [FromQuery]string code)
|
|
{
|
|
|
|
//We arrive here after the user has logged in and now we should have the authorization code that can now be used to fetch the actual tokens we need
|
|
var TokenEndpoint = DiscoveryDoc["token_endpoint"].Value<string>();
|
|
if (string.IsNullOrWhiteSpace(TokenEndpoint))
|
|
{
|
|
return Content($"<h1>Error - Unable to find TokenEndpoint value in Discovery document from QuickBooks Online</h1>Cannot proceed");
|
|
}
|
|
|
|
//Step 5: Exchange authorization code for refresh and access tokens
|
|
//Get access token
|
|
//which is step 5 here: https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/openid-connect#step-5-exchange-authorization-code-to-obtain-id-token-and-access-token
|
|
|
|
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, TokenEndpoint);
|
|
request.Headers.Add("Accept", "application/json");
|
|
request.Headers.Add("User-Agent", "AyaNova-QBridge");
|
|
request.Headers.Add("Authorization", "Basic " + Base64Encode(CLIENT_ID + ":" + CLIENT_SECRET));
|
|
|
|
var bodyParams = new Dictionary<string, string>()
|
|
{
|
|
{"code", code },
|
|
{"redirect_uri", REDIRECT_URI },
|
|
{"grant_type","authorization_code"}
|
|
};
|
|
request.Content = new FormUrlEncodedContent(bodyParams);
|
|
|
|
var client = _clientFactory.CreateClient();
|
|
var response = await client.SendAsync(request);
|
|
|
|
JObject AccessTokenObject = null;
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
string data = await response.Content.ReadAsStringAsync();
|
|
AccessTokenObject = JObject.Parse(data);
|
|
}
|
|
else
|
|
{
|
|
AccessTokenObject = null;
|
|
}
|
|
|
|
return Content($"TOKEN: {AccessTokenObject.ToString()}");
|
|
|
|
/*
|
|
Actual response example:
|
|
|
|
TOKEN: {
|
|
"access_token": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..jUV9qU1fnLn8U1KWBj7VuQ.0734yzJslW7Mp9pOIh6NTQZO-hEFJp_5R2JIJecQkJ866rWg9U3FVilwTaBmMEtC3AR4AxJva1nf_LU4JJNA2_EWtRxItHXbVsa54yk5-uELr-42IKjIXlWm1vWQQnkJRye8gZvy6LJAVmbL8exX3WIDHZjauObvLYEifgWAx2HhYrkWfTwY0T4trxxMOmjRMHARXsi-4VPTuMZNgSLEf0ipu7UVepb6lM4T0rXtUHTFwp1W4-dbwbihD7OX6eBS68LX4FIGno6kxMB89sGOf8JUEx6wKyTg7GBll64aOEAf6_hqkh7dIjyLmDXtfyQvB8K77QHTGpPVQOVw8O66_QSx4ePiI6WFhQtUTIdKSlO8w3pmcVcq-iEnAP2GJlwgg1zMBb83QX1LRICjsGMYZwSMNBPlNcuasBcKHJSa3TftTqcm1DB35Cn1CY7Ulte8y4ClkjXQztdL4fJRFUpMCSZ-QSYfgGCfhATM7YE1ErFaXhgpCwsYxACU6G7mDSC9AcTuCupAVSDOas9tuuqsndETahXrhrp83NZMukXgnhv9eC_ac3N6jcog-hjvzsvkNfjSEqyiMWaC3yNQx5Qp0LDCsd2Byx3Feg8QUqhmI8XK6on1fxyJeZxv4o3kyciSVF6KzgJMgB6mYjv-tQ2tpjvCxb2AkSoI2tRkUic9UoRUsWZBNOLmZr8nw6xZ_e1IYkU9gWkh0hmiWB648onghQPWIJOl5hak1a5HSaoEQbkL_ouZwIMWgM2M02GeFrEr.m1xIClqQIgBZyDDcjfPvAg",
|
|
"x_refresh_token_expires_in": 15552000,
|
|
"refresh_token": "AB11584991271bMYG1tOJ3YyUw7CeaeI2ctbrwD2kZQ6AXnAbd",
|
|
"token_type": "bearer",
|
|
"expires_in": 3600,
|
|
"id_token": "eyJraWQiOiJPUElDUFJEMDkxODIwMTQiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxNDQ5OGQwYi00YjcxLTQ0MDUtYmM1OS1hMWM2YmQ1YjVhZjAiLCJhdWQiOlsiQUJqNzBXdjVnRGF1RmQ5S2dLRnd1dnBRamZ6VHdFZ29kRUc4dG5CYlM4bVNRaE5yWkoiXSwiYXV0aF90aW1lIjoxNTY5NDMxODAxLCJpc3MiOiJodHRwczpcL1wvb2F1dGgucGxhdGZvcm0uaW50dWl0LmNvbVwvb3BcL3YxIiwiZXhwIjoxNTY5NDQyODcxLCJpYXQiOjE1Njk0MzkyNzF9.U6tjszRUnjBxktbb1pZoekotCSpxeHlHA-Gfy1RzEhAG0bj1gZlH5ksOhjaSE_PNx4WRKRL1fSvg5o59BTb-9iFyUZ7eiOF9Wny-C-kvxTlgnapX2H8TwNr5kKQWznuxwTzMzFVSq6REr5Ywad6B5tovzV5CG2Gan1tKxyR3ST0"
|
|
}
|
|
*/
|
|
|
|
//https://localhost:5001/oauthredirect?state=bar&code=foo
|
|
// return Content($"State: {state}, Code: {code}");
|
|
}
|
|
|
|
public static string Base64Encode(string plainText)
|
|
{
|
|
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
|
return System.Convert.ToBase64String(plainTextBytes);
|
|
}
|
|
|
|
public async Task GetQBDiscoveryDocument()
|
|
{
|
|
/*
|
|
|
|
issuer:"https://oauth.platform.intuit.com/op/v1",
|
|
authorization_endpoint:"https://appcenter.intuit.com/connect/oauth2",
|
|
token_endpoint:"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
|
|
userinfo_endpoint:"https://accounts.intuit.com/v1/openid_connect/userinfo",
|
|
revocation_endpoint:"https://developer.API.intuit.com/v2/oauth2/tokens/revoke",
|
|
jwks_uri:"https://oauth.platform.intuit.com/op/v1/jwks",
|
|
*/
|
|
var request = new HttpRequestMessage(HttpMethod.Get,
|
|
"https://developer.api.intuit.com/.well-known/openid_sandbox_configuration");
|
|
request.Headers.Add("Accept", "application/json");
|
|
request.Headers.Add("User-Agent", "AyaNova-QBridge");
|
|
|
|
var client = _clientFactory.CreateClient();
|
|
|
|
var response = await client.SendAsync(request);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
string data = await response.Content.ReadAsStringAsync();
|
|
DiscoveryDoc = JObject.Parse(data);
|
|
}
|
|
else
|
|
{
|
|
DiscoveryDoc = null;
|
|
}
|
|
return;
|
|
// string baseUrl = "https://developer.api.intuit.com/.well-known/openid_sandbox_configuration"; //The 'using' will help to prevent memory leaks. //Create a new instance of HttpClient
|
|
// using (System.Net.Http.HttpClient client = new HttpClient())
|
|
|
|
// //Setting up the response...
|
|
|
|
// using (HttpResponseMessage res = await client.GetAsync(baseUrl))
|
|
// using (HttpContent content = res.Content)
|
|
// {
|
|
// string data = await content.ReadAsStringAsync();
|
|
// if (data != null)
|
|
// {
|
|
// Console.WriteLine(data);
|
|
// }
|
|
// }
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("Revoke/{tokenToRevoke}")]
|
|
public async Task<IActionResult> RevokeAsync([FromRoute]string tokenToRevoke)
|
|
{
|
|
//Revoke the access token for the app for the unique ID specified
|
|
/*
|
|
|
|
POST https://developer.api.intuit.com/v2/oauth2/tokens/revoke HTTP/1.1
|
|
Accept: application/json
|
|
Authorization: Basic UTM0dVBvRDIwanp2OUdxNXE1dmlMemppcTlwM1d2
|
|
NzRUdDNReGkwZVNTTDhFRWwxb0g6VEh0WEJlR3dheEtZSlVNaFhzeGxma1l
|
|
XaFg3ZlFlRzFtN2szTFRwbw==
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"token": "{bearerToken or refreshToken}"
|
|
}
|
|
|
|
*/
|
|
|
|
//GET THE DISCOVERY DOCUMENT
|
|
//Discovery document contains the actual current endpoints to use for various ops
|
|
await GetQBDiscoveryDocument();
|
|
if (DiscoveryDoc == null)
|
|
{
|
|
return Content($"<h1>Error - Unable to fetch Discovery document from QuickBooks Online</h1>Cannot proceed with Revoke");
|
|
}
|
|
|
|
|
|
var revocation_endpoint = DiscoveryDoc["revocation_endpoint"].Value<string>();
|
|
if (string.IsNullOrWhiteSpace(revocation_endpoint))
|
|
{
|
|
return Content($"<h1>Error - Unable to find revocation_endpoint value in Discovery document from QuickBooks Online</h1>Cannot proceed");
|
|
}
|
|
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, revocation_endpoint);
|
|
request.Headers.Add("Accept", "application/json");
|
|
request.Headers.Add("User-Agent", "AyaNova-QBridge");
|
|
request.Headers.Add("Authorization", "Basic " + Base64Encode(CLIENT_ID + ":" + CLIENT_SECRET));
|
|
|
|
// var bodyParams = new Dictionary<string, string>()
|
|
// {
|
|
// {"code", code },
|
|
// {"redirect_uri", REDIRECT_URI },
|
|
// {"grant_type","authorization_code"}
|
|
// };
|
|
|
|
|
|
string jfrag = $"{{\"token\":\"{tokenToRevoke}\"}}";
|
|
|
|
request.Content = new StringContent(jfrag, System.Text.Encoding.UTF8, "application/json");
|
|
|
|
var client = _clientFactory.CreateClient();
|
|
|
|
var response = await client.SendAsync(request);
|
|
|
|
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
return Content("Token revoked");
|
|
}
|
|
else
|
|
{
|
|
return Content("Token revocation FAILED!");
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
Plan:
|
|
Make a web APP and api that runs on our server and handles getting tokens from the QB Online oAuth2 endpoints
|
|
|
|
Docs for normal development are here: https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization
|
|
|
|
Tentative process:
|
|
|
|
Borrowing from the technique and concepts outlined here: http://relasoft.net/KB10004.html
|
|
and here: https://github.com/IntuitDeveloper/C2QB-library-for-Windows-CUI-and-GUI/issues/1#issuecomment-511172847
|
|
|
|
|
|
User runs QBOI plugin, if it needs a new access token then it shells out to browser (with random temp session ID number to uniquely identify this user) to go to *our* qBridge auth page.
|
|
User enters their creds to login to QBOnline instance.
|
|
QBridge passes creds (along with random session id as the extra parameter they allow) on to the QBOI auth page which when successful redirects browser to the QBridge page we've specified as the
|
|
"redirect url" with the tokens in the url and also our unique session ID which then shows the end user that it's success and stores the tokens somewhere (gonna need a db I guess) for fetching by QBOI.
|
|
|
|
Meanwhile, in the background, QBOI is polling a route on qbridge with the unique ID number looking for a return of the tokens it needs to proceed.
|
|
Once it fetches the "Access token" and "Refresh token" it needs successfully then it continues on to normal usage
|
|
|
|
If it gets a response that the token needs to be refreshed, it either hands this operation off to qBridge or does it itself (not sure at this point which way it's supposed to happen)
|
|
If the access token expires after 100 days or so then they repeat this process (automatically by QBOI)
|
|
|
|
|
|
*/
|
|
// POST: api/Todo
|
|
// [HttpPost]
|
|
// public async Task<ActionResult<QItem>> Post(QCreds creds)
|
|
// {
|
|
|
|
// var q = new QItem();
|
|
// q.Token1 = "Test token 1";
|
|
// q.Token2 = System.DateTime.Now.ToString();
|
|
// q.Token3 = creds.Login;
|
|
// return Ok(q);
|
|
// //return CreatedAtAction(nameof(GetTodoItem), new { id = item.Id }, item);
|
|
// }
|
|
|
|
// public class QCreds
|
|
// {
|
|
// public string Login { get; set; }
|
|
// public string Password { get; set; }
|
|
// }
|
|
|
|
|
|
// *************************************************************************************************************
|
|
|
|
|
|
|
|
}
|
|
} |