diff --git a/OAuthDemo/Controllers/ChargeCreditCard.cs b/OAuthDemo/Controllers/ChargeCreditCard.cs index 9f57332..d0f1272 100644 --- a/OAuthDemo/Controllers/ChargeCreditCard.cs +++ b/OAuthDemo/Controllers/ChargeCreditCard.cs @@ -15,10 +15,10 @@ public static String Run(String AccessToken, String CardNumber, DateTime Expirat { Console.WriteLine("Charge Credit Card Sample"); - ApiOperationBase.RunEnvironment = AuthorizeNet.Environment.SANDBOX; - - // define the merchant information (authentication / transaction id) - ApiOperationBase.MerchantAuthentication = new merchantAuthenticationType() + // SECURITY: Authentication is set per-request on the request object (not via + // shared static properties) to prevent race conditions in multi-threaded + // ASP.NET environments. Environment is passed to Execute() for the same reason. + var merchantAuthentication = new merchantAuthenticationType() { ItemElementName = ItemChoiceType.accessToken, Item = AccessToken, @@ -58,11 +58,15 @@ public static String Run(String AccessToken, String CardNumber, DateTime Expirat lineItems = lineItems }; - var request = new createTransactionRequest { transactionRequest = transactionRequest }; + var request = new createTransactionRequest + { + merchantAuthentication = merchantAuthentication, + transactionRequest = transactionRequest + }; - // instantiate the contoller that will call the service + // instantiate the controller that will call the service var controller = new createTransactionController(request); - controller.Execute(); + controller.Execute(AuthorizeNet.Environment.SANDBOX); // get the response from the service (errors contained if any) var response = controller.GetApiResponse(); diff --git a/OAuthDemo/Controllers/DemoController.cs b/OAuthDemo/Controllers/DemoController.cs index f79a119..af59b8d 100644 --- a/OAuthDemo/Controllers/DemoController.cs +++ b/OAuthDemo/Controllers/DemoController.cs @@ -11,19 +11,51 @@ namespace OAuthDemo.Controllers { + // This educational demo is intentionally usable without a login. To still prevent + // IDOR (one visitor tampering with a Demo record created by another visitor), each + // Demo is bound to the browser Session that created it, and every action validates + // that the requested Id belongs to the current session before touching it. public class DemoController : Controller { + private const string OwnedDemoIdsKey = "OwnedDemoIds"; + private readonly ApplicationDbContext _context; public DemoController() { _context = new ApplicationDbContext(); } + + // The set of Demo Ids created by (and therefore owned by) the current session. + private HashSet OwnedDemoIds + { + get + { + var owned = Session[OwnedDemoIdsKey] as HashSet; + if (owned == null) + { + owned = new HashSet(); + Session[OwnedDemoIdsKey] = owned; + } + return owned; + } + } + + // Returns the Demo only if it belongs to the current session; otherwise null, + // so callers can reject the request instead of acting on someone else's record. + private Demo GetOwnedDemo(string id) + { + if (string.IsNullOrEmpty(id) || !OwnedDemoIds.Contains(id)) + return null; + return _context.Demos.SingleOrDefault(d => d.Id == id); + } + // GET: Demo public ActionResult Index() { Demo DemoModel = new Demo(Guid.NewGuid().ToString()); _context.Demos.Add(DemoModel); _context.SaveChanges(); + OwnedDemoIds.Add(DemoModel.Id); // record session ownership for IDOR checks return View(DemoModel); } @@ -33,16 +65,19 @@ public ActionResult RegisterApplication() } // step 2 - public ActionResult RedirectMerchant(Demo InputModel) + public ActionResult RedirectMerchant(RedirectMerchantInput input) { System.Diagnostics.Debug.WriteLine(_context.Demos.ToString()); - var SavedModel = _context.Demos.SingleOrDefault(d => d.Id == InputModel.Id); - SavedModel.ClientId = InputModel.ClientId; - SavedModel.RedirectUri = InputModel.RedirectUri; - SavedModel.Read = InputModel.Read; - SavedModel.Write = InputModel.Write; - SavedModel.State = InputModel.State; - SavedModel.Sub = InputModel.Sub; + var SavedModel = GetOwnedDemo(input.Id); + if (SavedModel == null) + return new HttpUnauthorizedResult(); + + SavedModel.ClientId = input.ClientId; + SavedModel.RedirectUri = input.RedirectUri; + SavedModel.Read = input.Read; + SavedModel.Write = input.Write; + SavedModel.State = input.State; + SavedModel.Sub = input.Sub; SavedModel.updateRedirectMerchantUrl(); _context.SaveChanges(); @@ -50,13 +85,18 @@ public ActionResult RedirectMerchant(Demo InputModel) } // step 3 - public ActionResult RetrieveAccessToken(Demo InputModel) + public ActionResult RetrieveAccessToken(RetrieveAccessTokenInput input) { - var SavedModel = _context.Demos.SingleOrDefault(d => d.Id == InputModel.Id); - SavedModel.GrantType = InputModel.GrantType; - SavedModel.Code = InputModel.Code; - SavedModel.ClientId = InputModel.ClientId; - SavedModel.ClientSecret = InputModel.ClientSecret; + var SavedModel = GetOwnedDemo(input.Id); + if (SavedModel == null) + return new HttpUnauthorizedResult(); + + SavedModel.GrantType = input.GrantType; + SavedModel.Code = input.Code; + SavedModel.ClientId = input.ClientId; + // WARNING: Demo application only - credentials stored in plaintext for educational purposes + // PRODUCTION CODE MUST encrypt credentials at rest using ASP.NET Data Protection API or column-level encryption + SavedModel.ClientSecret = input.ClientSecret; try { @@ -76,13 +116,16 @@ public ActionResult RetrieveAccessToken(Demo InputModel) } // step 4 - public ActionResult ChargeCreditCard(Demo InputModel) + public ActionResult ChargeCreditCard(ChargeCreditCardInput input) { - var SavedModel = _context.Demos.SingleOrDefault(d => d.Id == InputModel.Id); - SavedModel.AccessToken = InputModel.AccessToken; - SavedModel.CardNumber = InputModel.CardNumber; - SavedModel.ExpirationDate = InputModel.ExpirationDate; - SavedModel.Amount = InputModel.Amount; + var SavedModel = GetOwnedDemo(input.Id); + if (SavedModel == null) + return new HttpUnauthorizedResult(); + + SavedModel.AccessToken = input.AccessToken; + SavedModel.CardNumber = input.CardNumber; + SavedModel.ExpirationDate = input.ExpirationDate; + SavedModel.Amount = input.Amount; try { @@ -101,11 +144,14 @@ public ActionResult ChargeCreditCard(Demo InputModel) return View("Index", SavedModel); } - public ActionResult GetTransactionDetails(Demo InputModel) + public ActionResult GetTransactionDetails(GetTransactionDetailsInput input) { - var SavedModel = _context.Demos.SingleOrDefault(d => d.Id == InputModel.Id); - SavedModel.AccessToken = InputModel.AccessToken; - SavedModel.TransactionId = InputModel.TransactionId; + var SavedModel = GetOwnedDemo(input.Id); + if (SavedModel == null) + return new HttpUnauthorizedResult(); + + SavedModel.AccessToken = input.AccessToken; + SavedModel.TransactionId = input.TransactionId; try { @@ -124,13 +170,16 @@ public ActionResult GetTransactionDetails(Demo InputModel) } // step 5 - public ActionResult RefreshAccessToken(Demo InputModel) + public ActionResult RefreshAccessToken(RefreshAccessTokenInput input) { - var SavedModel = _context.Demos.SingleOrDefault(d => d.Id == InputModel.Id); - SavedModel.ClientId = InputModel.ClientId; - SavedModel.ClientSecret = InputModel.ClientSecret; - SavedModel.GrantType = InputModel.GrantType; - SavedModel.RefreshToken = InputModel.RefreshToken; + var SavedModel = GetOwnedDemo(input.Id); + if (SavedModel == null) + return new HttpUnauthorizedResult(); + + SavedModel.ClientId = input.ClientId; + SavedModel.ClientSecret = input.ClientSecret; + SavedModel.GrantType = input.GrantType; + SavedModel.RefreshToken = input.RefreshToken; try { @@ -152,4 +201,4 @@ public ActionResult RedirectRevokePermissions() return Redirect(Demo.RevokePermissionsUrl); } } -} \ No newline at end of file +} diff --git a/OAuthDemo/Controllers/GetTransactionDetails.cs b/OAuthDemo/Controllers/GetTransactionDetails.cs index cd2f507..a166a58 100644 --- a/OAuthDemo/Controllers/GetTransactionDetails.cs +++ b/OAuthDemo/Controllers/GetTransactionDetails.cs @@ -15,20 +15,22 @@ public static String Run(String AccessToken, string transactionId) { Console.WriteLine("Get transaction details sample"); - ApiOperationBase.RunEnvironment = AuthorizeNet.Environment.SANDBOX; - // define the merchant information (authentication / transaction id) - ApiOperationBase.MerchantAuthentication = new merchantAuthenticationType() + // SECURITY: Authentication is set per-request on the request object (not via + // shared static properties) to prevent race conditions in multi-threaded + // ASP.NET environments. Environment is passed to Execute() for the same reason. + var merchantAuthentication = new merchantAuthenticationType() { ItemElementName = ItemChoiceType.accessToken, Item = AccessToken }; var request = new getTransactionDetailsRequest(); + request.merchantAuthentication = merchantAuthentication; request.transId = transactionId; // instantiate the controller that will call the service var controller = new getTransactionDetailsController(request); - controller.Execute(); + controller.Execute(AuthorizeNet.Environment.SANDBOX); // get the response from the service (errors contained if any) var response = controller.GetApiResponse(); diff --git a/OAuthDemo/Models/DemoInputModels.cs b/OAuthDemo/Models/DemoInputModels.cs new file mode 100644 index 0000000..0667b18 --- /dev/null +++ b/OAuthDemo/Models/DemoInputModels.cs @@ -0,0 +1,54 @@ +using System; + +namespace OAuthDemo.Models +{ + // Input model for RedirectMerchant action + public class RedirectMerchantInput + { + public string Id { get; set; } + public string ClientId { get; set; } + public string RedirectUri { get; set; } + public bool Read { get; set; } + public bool Write { get; set; } + public string State { get; set; } + public string Sub { get; set; } + } + + // Input model for RetrieveAccessToken action + public class RetrieveAccessTokenInput + { + public string Id { get; set; } + public string GrantType { get; set; } + public string Code { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + } + + // Input model for ChargeCreditCard action + public class ChargeCreditCardInput + { + public string Id { get; set; } + public string AccessToken { get; set; } + public string CardNumber { get; set; } + public DateTime ExpirationDate { get; set; } + public decimal Amount { get; set; } + } + + // Input model for GetTransactionDetails action + public class GetTransactionDetailsInput + { + public string Id { get; set; } + public string AccessToken { get; set; } + public string TransactionId { get; set; } + } + + // Input model for RefreshAccessToken action + public class RefreshAccessTokenInput + { + public string Id { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string GrantType { get; set; } + public string RefreshToken { get; set; } + } +} diff --git a/OAuthDemo/Models/DemoModels.cs b/OAuthDemo/Models/DemoModels.cs index 508920d..3763dbe 100644 --- a/OAuthDemo/Models/DemoModels.cs +++ b/OAuthDemo/Models/DemoModels.cs @@ -7,6 +7,12 @@ namespace OAuthDemo.Models { + // WARNING: This is a demonstration application for educational purposes only + // OAuth credentials are stored in plaintext in the database to simplify the demo + // PRODUCTION APPLICATIONS MUST encrypt sensitive data at rest using: + // - ASP.NET Data Protection API + // - SQL Server column-level encryption + // - Azure Key Vault or similar key management service public class Demo { public const string RetrieveErrorResponse = "Error Retrieving the Access Token"; @@ -22,8 +28,11 @@ static Demo() public Demo() { - ClientId = "4dp5b7gRqk"; - ClientSecret = "fa3a5b16753d09b24bb44243605a4a98"; + // ClientId / ClientSecret must come from Web.config (use placeholders + // [YOUR_CLIENT_ID] / [YOUR_CLIENT_SECRET] until you fill in your own sandbox values). + // Obtain values from the Authorize.Net Sandbox portal — see README for the procedure. + ClientId = ConfigurationManager.AppSettings["ClientId"] ?? ""; + ClientSecret = ConfigurationManager.AppSettings["ClientSecret"] ?? ""; RedirectUri = "https://developer.authorize.net/api/reference/index.html"; Read = true; Write = true; @@ -108,4 +117,4 @@ override public string ToString() "\n"; } } -} \ No newline at end of file +} diff --git a/OAuthDemo/Views/Demo/Index.cshtml b/OAuthDemo/Views/Demo/Index.cshtml index bbc8370..e502d50 100644 --- a/OAuthDemo/Views/Demo/Index.cshtml +++ b/OAuthDemo/Views/Demo/Index.cshtml @@ -36,7 +36,7 @@ @Html.LabelFor(m => m.ClientSecret, new {@class = "col-md-2 control-label"})
- @Html.TextBoxFor(m => m.ClientSecret, new {@class = "form-control", Value = Model.ClientSecret}) + @Html.PasswordFor(m => m.ClientSecret, new {@class = "form-control"})
@@ -122,7 +122,7 @@
@Html.LabelFor(m => m.ClientSecret, new {@class = "col-md-2 control-label"})
- @Html.TextBoxFor(m => m.ClientSecret, new {@class = "form-control"}) + @Html.PasswordFor(m => m.ClientSecret, new {@class = "form-control"})
@@ -134,7 +134,7 @@

Response:

- @Html.TextArea("Step3Response", null, new {rows = 10, cols = 500, value = Model.Step3Response}) + @Html.TextArea("Step3Response", null, new {rows = 10, cols = 500, value = Model.Step3Response, @readonly = "readonly"})
Continue @@ -160,11 +160,11 @@
@Html.LabelFor(m => m.AccessToken, new {@class = "col-md-2 control-label"})
- @Html.TextBoxFor(m => m.AccessToken, new {@class = "form-control"}) + @Html.PasswordFor(m => m.AccessToken, new {@class = "form-control"})
@Html.LabelFor(m => m.CardNumber, new {@class = "col-md-2 control-label"})
- @Html.TextBoxFor(m => m.CardNumber, new {@class = "form-control"}) + @Html.PasswordFor(m => m.CardNumber, new {@class = "form-control"})
@Html.LabelFor(m => m.ExpirationDate, new {@class = "col-md-2 control-label"})
@@ -195,7 +195,7 @@
@Html.LabelFor(m => m.AccessToken, new { @class = "col-md-2 control-label" })
- @Html.TextBoxFor(m => m.AccessToken, new {@class = "form-control"}) + @Html.PasswordFor(m => m.AccessToken, new {@class = "form-control"})
@Html.LabelFor(m => m.TransactionId, new { @class = "col-md-2 control-label" })
@@ -235,7 +235,7 @@
@Html.LabelFor(m => m.ClientSecret, new { @class = "col-md-2 control-label" })
- @Html.TextBoxFor(m => m.ClientSecret, new { @class = "form-control"}) + @Html.PasswordFor(m => m.ClientSecret, new { @class = "form-control"})
@Html.LabelFor(m => m.GrantType, new { @class = "col-md-2 control-label" })
@@ -243,7 +243,7 @@
@Html.LabelFor(m => m.RefreshToken, new { @class = "col-md-2 control-label" })
- @Html.TextBoxFor(m => m.RefreshToken, new { @class = "form-control" }) + @Html.PasswordFor(m => m.RefreshToken, new { @class = "form-control" })
diff --git a/OAuthDemo/Web.config b/OAuthDemo/Web.config index eab1933..f11d5f5 100644 --- a/OAuthDemo/Web.config +++ b/OAuthDemo/Web.config @@ -13,6 +13,13 @@ + + + @@ -91,4 +98,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index b43e523..d69aeb6 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,51 @@ # OAuth Sample Application -This repository contains a sample application which demonstrates connecting to Authorize.Net using the OAuth 2.0 authentication standard. +This repository contains a **sample / educational application** that demonstrates how to integrate with the Authorize.Net **OAuth 2.0 sandbox** flow from an ASP.NET MVC application. It is **not a production application**, **not a hosted service**, and **not part of the main Authorize.Net product**. It is intended for developers who want to see, build, and run an OAuth 2.0 reference integration **locally** while learning the Authorize.Net APIs. + +> ### ⚠️ Scope & Intended Use +> +> - **Purpose:** Educational reference / integration showcase only. +> - **Deployment:** Designed to be cloned and run **locally** (Visual Studio + IIS Express). It is _not_ deployed to any hosted environment, and the project ships with no production URL or production endpoint. +> - **Backend target:** Authorize.Net **Sandbox only** (`https://sandbox.authorize.net`). There is no production URL anywhere in the codebase. +> - **Data:** Uses an attached `LocalDb` (`(LocalDb)\MSSQLLocalDB`) on the developer's own machine. No shared database, no production data. +> - **Credentials:** Sample / placeholder values only (e.g., `[YOUR_CLIENT_ID]`, `[YOUR_CLIENT_SECRET]`). Real credentials must be obtained by the developer from the Authorize.Net Sandbox portal and used **only locally** in `Web.config` ``. ## **How to Use the Sample Application?** - Clone or download this repository. -- Open solution OAuthDemo.sln in Visual Studio and set OAuthDemo as StartUp project -- Run OAuthDemo.sln from Visual studio to launch the application. Application runs on local IIS server. +- Open solution OAuthDemo.sln in Visual Studio and set OAuthDemo as StartUp project. +- Open `OAuthDemo/Web.config` and **replace the `[YOUR_CLIENT_ID]` and `[YOUR_CLIENT_SECRET]` placeholders** with values you obtain from the Authorize.Net Sandbox portal (see below). +- Run OAuthDemo.sln from Visual studio to launch the application. Application runs on the local IIS Express server. -![alt text](https://github.com/AuthorizeNet/oauth-sample-app/blob/master/OAuthDemo/Screenshots/Image1.png ) +![alt text](https://github.com/AuthorizeNet/oauth-sample-app/blob/master/OAuthDemo/Screenshots/Image1.png) -- ClientId and ClientSecret values can be obtained by contacting Authorize.Net support team at [affiliate@authorize.net](mailto:affiliate@authorize.net) and providing a RedirectUri(This is the page that the merchant is redirected back to after granting permissions) for your application. +### Obtaining your sandbox `ClientId` and `ClientSecret` -Sample ClientId and ClientSecret shown in the below screen can be used for the demo purpose and can later be replaced in the code with newly obtained ClientId and ClientSecret. +You must use **your own** Authorize.Net **sandbox** credentials. This repository deliberately does **not** ship working credentials. -File - DemoModel.cs +1. Sign in to the Authorize.Net sandbox: **https://sandbox.authorize.net** +2. Navigate to **Account → Settings → Security Settings → API Credentials & Keys**. +3. Generate / copy your **Client ID** and **Client Secret**. +4. Provide a **RedirectUri** (the page the merchant is redirected back to after granting permissions) when registering your application. +5. Place the values into `OAuthDemo/Web.config`: -public Demo() +```xml + + +``` - { +> If you have any questions on credentialing, you can reach the Authorize.Net partner team at [affiliate@authorize.net](mailto:affiliate@authorize.net). - ClientId = "4dp5b7gRqk"; +`OAuthDemo/Models/DemoModels.cs` reads these values via `ConfigurationManager.AppSettings["ClientId"]` / `["ClientSecret"]`. The application falls back to an empty string if the key is missing — there are no hardcoded credentials anywhere in the source tree. - ClientSecret = "fa3a5b16753d09b24bb44243605a4a98"; +```csharp +public Demo() +{ + ClientId = ConfigurationManager.AppSettings["ClientId"] ?? ""; + ClientSecret = ConfigurationManager.AppSettings["ClientSecret"] ?? ""; + // … +} +``` ![alt text](https://github.com/AuthorizeNet/oauth-sample-app/blob/master/OAuthDemo/Screenshots/Image2.png ) @@ -40,7 +63,7 @@ public Demo() ![alt text](https://github.com/AuthorizeNet/oauth-sample-app/blob/master/OAuthDemo/Screenshots/Image4.png ) -- Login with your Authorize.net credentials to allow access. +- Login with your Authorize.net credentials to allow access. ![alt text](https://github.com/AuthorizeNet/oauth-sample-app/blob/master/OAuthDemo/Screenshots/Image5.png ) - Click Allow. Page will be redirected to [https://developer.authorize.net](https://developer.authorize.net) with generated authorization code in the url. Copy the authorization code to obtain access and refresh token. @@ -102,12 +125,11 @@ Refresh Token is revoked immediately. Any previously issued Access Token will be -Note: If the OAuthDemo application is running on a network which is behind a proxy, you may have to add below settings in the web.config file of the OAuth Demo application project to access the API endpoint. - -<system.net> - - <defaultProxyuseDefaultCredentials="true"enabled="true"> - - </defaultProxy> +Note: If the OAuthDemo application is running on a network which is behind a proxy, you may have to add the settings below to the `Web.config` file of the OAuth Demo application project to access the API endpoint. -</system.net> +```xml + + + + +``` diff --git a/csharp-client/src/IO.Swagger.Test/Api/RetrievingrefreshingApiTests.cs b/csharp-client/src/IO.Swagger.Test/Api/RetrievingrefreshingApiTests.cs index a09a8d7..ad9b321 100644 --- a/csharp-client/src/IO.Swagger.Test/Api/RetrievingrefreshingApiTests.cs +++ b/csharp-client/src/IO.Swagger.Test/Api/RetrievingrefreshingApiTests.cs @@ -70,11 +70,13 @@ public void InstanceTest() [Test] public void GetTokenTest() { - // TODO uncomment below to test the method and replace null with proper value + // TODO uncomment below to test the method and replace placeholders with values + // obtained from the Authorize.Net Sandbox portal: + // https://sandbox.authorize.net → Account → Settings → Security Settings → API Credentials & Keys string grantType = "authorization_code"; - string clientId = "4dp5b7gRqk"; - string code = "novp2e"; - string clientSecret = "fa3a5b16753d09b24bb44243605a4a98"; + string clientId = "[YOUR_CLIENT_ID]"; + string code = "[YOUR_AUTHORIZATION_CODE]"; + string clientSecret = "[YOUR_CLIENT_SECRET]"; string refreshToken = null; int? platform = 2; var response = instance.GetToken(grantType, clientId, code, clientSecret, refreshToken, platform);