├── .github ├── dependabot.yml └── workflows │ └── NetFx.yml ├── .gitignore ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── src └── AccountSecurity ├── AccountSecurity.csproj ├── Controllers ├── AccountSecurityController.cs ├── BaseController.cs ├── HomeController.cs ├── PhoneVerificationController.cs └── UserController.cs ├── Extensions └── SessionExtensions.cs ├── Migrations ├── 20181017215715_InitialCreate.Designer.cs ├── 20181017215715_InitialCreate.cs └── AccountSecurityContextModelSnapshot.cs ├── Models ├── AccountSecurityContext.cs ├── AppSettings.cs ├── LoginViewModel.cs ├── PhoneVerificationRequestModel.cs ├── RegisterViewModel.cs ├── TokenVerificationModel.cs └── applicationUser.cs ├── Program.cs ├── Services ├── AuthMessageSenderOptions.cs ├── Authy.cs └── EmailSender.cs ├── Startup.cs ├── Views └── Home │ ├── Index.cshtml │ ├── Login.cshtml │ ├── Protected.cshtml │ ├── Register.cshtml │ ├── TwoFactorSample.cshtml │ ├── Verification.cshtml │ └── Verified.cshtml ├── appsettings.development.json ├── appsettings.json ├── global.json ├── package-lock.json ├── package.json └── wwwroot ├── app.js ├── assets ├── favicon-16x16.png ├── favicon-32x32.png └── favicon.ico ├── index.html └── partials └── links.html /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/src/AccountSecurity" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: Microsoft.AspNetCore.Identity.UI 10 | versions: 11 | - 5.0.2 12 | - 5.0.3 13 | - 5.0.4 14 | - dependency-name: Microsoft.AspNetCore.Mvc.NewtonsoftJson 15 | versions: 16 | - 5.0.2 17 | - 5.0.3 18 | - 5.0.4 19 | - dependency-name: Microsoft.EntityFrameworkCore.Tools 20 | versions: 21 | - 5.0.2 22 | - 5.0.3 23 | - 5.0.4 24 | - dependency-name: Microsoft.EntityFrameworkCore.SqlServer 25 | versions: 26 | - 5.0.2 27 | - 5.0.3 28 | - 5.0.4 29 | - dependency-name: Microsoft.AspNetCore.Identity.EntityFrameworkCore 30 | versions: 31 | - 5.0.2 32 | - 5.0.3 33 | - dependency-name: Microsoft.EntityFrameworkCore.Design 34 | versions: 35 | - 5.0.2 36 | - 5.0.3 37 | - dependency-name: Microsoft.EntityFrameworkCore 38 | versions: 39 | - 5.0.2 40 | - 5.0.3 41 | - dependency-name: Microsoft.VisualStudio.Web.CodeGeneration.Design 42 | versions: 43 | - 5.0.1 44 | - dependency-name: Sendgrid 45 | versions: 46 | - 9.22.0 47 | -------------------------------------------------------------------------------- /.github/workflows/NetFx.yml: -------------------------------------------------------------------------------- 1 | name: NetFx 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 3.1.x 20 | - name: Restore dependencies 21 | run: dotnet restore src/AccountSecurity 22 | - name: Build 23 | run: dotnet build src/AccountSecurity --no-restore 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | _site/ 3 | Properties/ 4 | 5 | # Use git add -f to force override .sln when required. Not needed in most cases. 6 | # git add -f myProj.sln 7 | *.sln 8 | 9 | Project_Readme.html 10 | 11 | ## Ignore Visual Studio temporary files, build results, and 12 | ## files generated by popular Visual Studio add-ons. 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | *.vscode/ 20 | # User-specific files (MonoDevelop/Xamarin Studio) 21 | *.userprefs 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | build/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | 35 | # Visual Studo 2015 cache/options directory 36 | .vs/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opensdf 84 | *.sdf 85 | *.cachefile 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding addin-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | # TODO: Comment the next line if you want to checkin your web deploy settings 143 | # but database connection strings (with potential passwords) will be unencrypted 144 | *.pubxml 145 | *.publishproj 146 | 147 | # NuGet Packages 148 | *.nupkg 149 | # The packages folder can be ignored because of Package Restore 150 | **/packages/* 151 | # except build/, which is used as an MSBuild target. 152 | !**/packages/build/ 153 | # Uncomment if necessary however generally it will be regenerated when needed 154 | #!**/packages/repositories.config 155 | 156 | # Windows Azure Build Output 157 | csx/ 158 | *.build.csdef 159 | 160 | # Windows Store app package directory 161 | AppPackages/ 162 | 163 | # Others 164 | *.[Cc]ache 165 | ClientBin/ 166 | [Ss]tyle[Cc]op.* 167 | ~$* 168 | *~ 169 | *.dbmdl 170 | *.dbproj.schemaview 171 | *.pfx 172 | *.publishsettings 173 | node_modules/ 174 | bower_components/ 175 | 176 | # RIA/Silverlight projects 177 | Generated_Code/ 178 | 179 | # Backup & report files from converting an old project file 180 | # to a newer Visual Studio version. Backup files are not needed, 181 | # because we have git ;-) 182 | _UpgradeReport_Files/ 183 | Backup*/ 184 | UpgradeLog*.XML 185 | UpgradeLog*.htm 186 | 187 | # SQL Server files 188 | *.mdf 189 | *.ldf 190 | 191 | # Business Intelligence projects 192 | *.rdl.data 193 | *.bim.layout 194 | *.bim_*.settings 195 | 196 | # Microsoft Fakes 197 | FakesAssemblies/ 198 | 199 | # Node.js Tools for Visual Studio 200 | .ntvs_analysis.dat 201 | 202 | # Visual Studio 6 build log 203 | *.plg 204 | 205 | # Visual Studio 6 workspace options file 206 | *.opt 207 | 208 | project.lock.json 209 | __pycache__/ 210 | 211 | #Mac OSX 212 | .DS_Store 213 | 214 | # Windows thumbnail cache files 215 | Thumbs.db 216 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - author~=^dependabot(|-preview)\[bot\]$ 5 | - status-success=build 6 | actions: 7 | merge: 8 | method: squash 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Twilio 2 | 3 | All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Twilio Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Important Notice 6 | 7 | For new development, we encourage you to use the Verify API instead of the Authy API. The Verify API is an evolution of the Authy API with continued support for SMS, voice, and email one-time passcodes, an improved developer experience and new features. 8 | 9 | Please visit the [Verify Quickstarts Page](https://www.twilio.com/docs/verify/quickstarts) to get started with the Verify API. Thank you! 10 | 11 | 12 | # Two-Factor Authentication with ASP.NET Core and Authy 13 | 14 | ![](https://github.com/TwilioDevEd/account-security-csharp/workflows/NetFx/badge.svg) 15 | 16 | Here you will learn how to create a login system for ASP.NET Core applications secured with 2FA using Authy. 17 | 18 | [Learn more about this code in our docs](https://www.twilio.com/docs/authy/quickstart/dotnet-core-csharp-two-factor-authentication). 19 | 20 | ## Quickstart 21 | 22 | 23 | ### Create an Authy app 24 | 25 | Enable Authy in your [Twilio Account](https://www.twilio.com/authy/) if you don't 26 | have one already, and then connect it to your Twilio account. 27 | 28 | ### Local development 29 | 30 | This project is built using [.NET Core](https://www.microsoft.com/net/download), which will need to be installed before continuing. 31 | 32 | 1. First clone this repository and `cd` into it. 33 | 34 | ```bash 35 | git clone https://github.com/TwilioDevEd/account-security-csharp.git 36 | cd account-security-csharp 37 | ``` 38 | 39 | 1. Install the dependencies. 40 | 41 | ```bash 42 | cd src/AccountSecurity 43 | dotnet restore 44 | ``` 45 | #### MSSQL Server 46 | 47 | You can either install it or run it in a docker container. 48 | 49 | - [Install MSSQL Server Express](https://www.microsoft.com/en-us/sql-server/sql-server-editions-express) 50 | 51 | - Run in a docker container: 52 | 53 | `docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 --name mssql -d microsoft/mssql-server-linux:latest` 54 | 55 | 1. Make sure your `DefaultConnection` connection string is correct for your SQL Server installation. (You may need to change the `Server` to `localhost\\SQLEXPRESS` if running MSSQL Server Express on Windows.) 56 | 57 | 1. Install the Entity Framework tool: 58 | ```bash 59 | dotnet tool install --global dotnet-ef --version 3.1.4 60 | ``` 61 | 62 | 1. Run the database migrations: 63 | 64 | ```bash 65 | dotnet ef database update -v 66 | ``` 67 | 68 | 1. Set your Authy App API Key in your `appsettings.json` as `AuthyApiKey` which should be found under your [Authy App Settings](https://www.twilio.com/console/authy/applications). 69 | 70 | 1. Run the server. 71 | 72 | ```bash 73 | dotnet run --environment development 74 | ``` 75 | 76 | 1. Expose your application to the wider internet using [ngrok](http://ngrok.com). You can click 77 | [here](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html) for more details. This step 78 | is important because the application won't work as expected if you run it through localhost. 79 | 80 | ```bash 81 | ngrok http 5000 82 | ``` 83 | 84 | Once ngrok is running, open up your browser and go to your ngrok URL. 85 | It will look something like this: `http://9a159ccf.ngrok.io` 86 | 87 | ## Meta 88 | 89 | * No warranty expressed or implied. Software is as is. 90 | * [MIT License](http://www.opensource.org/licenses/mit-license.html) 91 | * Lovingly crafted by Twilio Developer Education. 92 | -------------------------------------------------------------------------------- /src/AccountSecurity/AccountSecurity.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AccountSecurity/Controllers/AccountSecurityController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using AccountSecurity.Models; 5 | using AccountSecurity.Services; 6 | using AccountSecurity.Extensions; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Logging; 11 | using Newtonsoft.Json; 12 | using Newtonsoft.Json.Linq; 13 | 14 | namespace AccountSecurity { 15 | 16 | [ 17 | Authorize, 18 | Route("/api/accountsecurity"), 19 | Produces("application/json") 20 | ] 21 | public class AccountSecurityController : BaseController 22 | { 23 | public UserManager userManager; 24 | public SignInManager signInManager; 25 | public ILogger logger; 26 | public IAuthy authy; 27 | 28 | public AccountSecurityController( 29 | UserManager userManager, 30 | SignInManager signInManager, 31 | ILoggerFactory loggerFactory, 32 | IAuthy authy) 33 | { 34 | this.userManager = userManager; 35 | this.signInManager = signInManager; 36 | this.logger = loggerFactory.CreateLogger(); 37 | this.authy = authy; 38 | } 39 | 40 | internal async Task> addTokenVerificationClaim(ApplicationUser user) { 41 | var tokenVerificationClaim = new Claim(ClaimTypes.AuthenticationMethod, "TokenVerification"); 42 | var claims = new List(); 43 | claims.Add(tokenVerificationClaim); 44 | 45 | var userClaims = (List)await userManager.GetClaimsAsync(user); 46 | 47 | if (userClaims.FindIndex(claim => claim.Value.Equals("TokenVerification")) == -1) { 48 | await userManager.AddClaimsAsync(user, claims); 49 | } 50 | 51 | return await userManager.GetClaimsAsync(user); 52 | } 53 | 54 | [HttpPost("verify")] 55 | public async Task verify([FromBody]TokenVerificationModel data) 56 | { 57 | var currentUser = await userManager.GetUserAsync(this.User); 58 | 59 | if (ModelState.IsValid) 60 | { 61 | TokenVerificationResult result; 62 | 63 | if(data.Token.Length > 4) { 64 | result = await authy.verifyTokenAsync(currentUser.AuthyId, data.Token); 65 | } else { 66 | result = await authy.verifyPhoneTokenAsync(currentUser.PhoneNumber, currentUser.CountryCode, data.Token); 67 | } 68 | 69 | logger.LogDebug(result.ToString()); 70 | 71 | if (result.Succeeded) 72 | { 73 | await addTokenVerificationClaim(currentUser); 74 | return Ok(result); 75 | } else { 76 | return BadRequest(result); 77 | } 78 | 79 | } else { 80 | return BadRequest(ModelState); 81 | } 82 | } 83 | 84 | [HttpPost("sms")] 85 | public async Task sms() 86 | { 87 | var currentUser = await userManager.GetUserAsync(this.User); 88 | var result = await authy.sendSmsAsync(currentUser.AuthyId); 89 | return Ok(result); 90 | } 91 | 92 | [HttpPost("voice")] 93 | public async Task voice() 94 | { 95 | var currentUser = await userManager.GetUserAsync(this.User); 96 | var result = await authy.phoneVerificationCallRequestAsync( 97 | currentUser.CountryCode, 98 | currentUser.PhoneNumber 99 | ); 100 | return Ok(result); 101 | } 102 | 103 | [HttpPost("onetouch")] 104 | public async Task oneTouch() 105 | { 106 | var currentUser = await userManager.GetUserAsync(this.User); 107 | var onetouch_uuid = await authy.createApprovalRequestAsync(currentUser.AuthyId); 108 | HttpContext.Session.Set("onetouch_uuid", onetouch_uuid); 109 | return Ok(); 110 | } 111 | 112 | [HttpPost("onetouchstatus")] 113 | public async Task oneTouchStatus() 114 | { 115 | var currentUser = await userManager.GetUserAsync(this.User); 116 | var onetouch_uuid = HttpContext.Session.Get("onetouch_uuid"); 117 | 118 | var result = await authy.checkRequestStatusAsync(onetouch_uuid); 119 | var data = (JObject)result; 120 | var approval_request_status = (string)data["approval_request"]["status"]; 121 | 122 | if (approval_request_status == "approved") { 123 | await addTokenVerificationClaim(currentUser); 124 | await userManager.UpdateAsync(currentUser); 125 | } 126 | 127 | 128 | return Ok(result); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace AccountSecurity { 5 | 6 | public class BaseController : ControllerBase 7 | { 8 | internal void AddErrors(IdentityResult result) 9 | { 10 | foreach (var error in result.Errors) 11 | { 12 | ModelState.AddModelError(string.Empty, error.Description); 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using AccountSecurity.Models; 3 | using Microsoft.AspNetCore.Authentication.Cookies; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace AccountSecurity { 9 | [Route("/")] 10 | public class HomeController : Controller 11 | { 12 | public SignInManager signInManager; 13 | 14 | public HomeController(SignInManager signInManager) { 15 | this.signInManager = signInManager; 16 | } 17 | 18 | [HttpGet] 19 | public IActionResult Index() 20 | { 21 | return View(); 22 | } 23 | 24 | [AllowAnonymous, HttpGet("register")] 25 | public IActionResult Register() 26 | { 27 | return View(); 28 | } 29 | 30 | [AllowAnonymous, HttpGet("login")] 31 | public IActionResult Login() 32 | { 33 | return View(); 34 | } 35 | 36 | [Authorize, HttpGet("logout")] 37 | public async Task Logout() { 38 | await signInManager.SignOutAsync(); 39 | return RedirectToAction("Index", "Home"); 40 | } 41 | 42 | [HttpGet("verification")] 43 | public IActionResult Verification() 44 | { 45 | return View(); 46 | } 47 | 48 | [HttpGet("verified")] 49 | public IActionResult Verified() 50 | { 51 | return View(); 52 | } 53 | 54 | [Authorize, HttpGet("2fa")] 55 | public IActionResult TwoFactorSample() 56 | { 57 | return View(); 58 | } 59 | 60 | [Authorize, HttpGet("protected")] 61 | public IActionResult Protected() 62 | { 63 | return View(); 64 | } 65 | 66 | [HttpGet("error")] 67 | public IActionResult Error() 68 | { 69 | return View(); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Controllers/PhoneVerificationController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using AccountSecurity.Models; 5 | using AccountSecurity.Services; 6 | using AccountSecurity.Extensions; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Logging; 11 | using Newtonsoft.Json; 12 | using Newtonsoft.Json.Linq; 13 | 14 | namespace AccountSecurity { 15 | 16 | [ 17 | Route("/api/verification"), 18 | Produces("application/json") 19 | ] 20 | public class PhoneVerificationController : BaseController 21 | { 22 | public UserManager userManager; 23 | public SignInManager signInManager; 24 | public ILogger logger; 25 | public IAuthy authy; 26 | 27 | public PhoneVerificationController( 28 | UserManager userManager, 29 | SignInManager signInManager, 30 | ILoggerFactory loggerFactory, 31 | IAuthy authy) 32 | { 33 | this.userManager = userManager; 34 | this.signInManager = signInManager; 35 | this.logger = loggerFactory.CreateLogger(); 36 | this.authy = authy; 37 | } 38 | 39 | [HttpPost("start")] 40 | public async Task start([FromBody]PhoneVerificationRequestModel verificationRequest) 41 | { 42 | HttpContext.Session.Set("phone_verification_request", verificationRequest); 43 | 44 | if (ModelState.IsValid) 45 | { 46 | string result; 47 | if (verificationRequest.via == Verification.SMS) { 48 | result = await authy.phoneVerificationRequestAsync( 49 | verificationRequest.CountryCode, 50 | verificationRequest.PhoneNumber 51 | ); 52 | } else { 53 | result = await authy.phoneVerificationCallRequestAsync( 54 | verificationRequest.CountryCode, 55 | verificationRequest.PhoneNumber 56 | ); 57 | } 58 | return Ok(result); 59 | 60 | } else { 61 | return BadRequest(ModelState); 62 | } 63 | } 64 | 65 | [HttpPost("verify")] 66 | public async Task verify([FromBody]TokenVerificationModel tokenVerification) 67 | { 68 | var verificationRequest = HttpContext.Session.Get("phone_verification_request"); 69 | 70 | if (ModelState.IsValid) 71 | { 72 | var validationResult = await authy.verifyPhoneTokenAsync( 73 | verificationRequest.PhoneNumber, 74 | verificationRequest.CountryCode, 75 | tokenVerification.Token 76 | ); 77 | 78 | return Ok(validationResult); 79 | 80 | } else { 81 | return BadRequest(ModelState); 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using AccountSecurity.Models; 3 | using AccountSecurity.Services; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | 11 | namespace AccountSecurity { 12 | 13 | [Produces("application/json")] 14 | [Route("/api/user")] 15 | public class UserController : BaseController 16 | { 17 | public UserManager userManager; 18 | public SignInManager signInManager; 19 | public ILogger logger; 20 | public IAuthy authy; 21 | 22 | public UserController( 23 | UserManager userManager, 24 | SignInManager signInManager, 25 | ILoggerFactory loggerFactory, 26 | IAuthy authy) 27 | { 28 | this.userManager = userManager; 29 | this.signInManager = signInManager; 30 | this.logger = loggerFactory.CreateLogger(); 31 | this.authy = authy; 32 | } 33 | 34 | [HttpPost("register")] 35 | [AllowAnonymous] 36 | public async Task> Register([FromBody]RegisterViewModel model) 37 | { 38 | if (ModelState.IsValid) 39 | { 40 | var user = new ApplicationUser { 41 | UserName = model.UserName, 42 | Email = model.Email, 43 | CountryCode = model.CountryCode, 44 | PhoneNumber = model.PhoneNumber, 45 | AuthyId = await authy.registerUserAsync(model) 46 | }; 47 | 48 | 49 | var result = await this.userManager.CreateAsync(user, model.Password); 50 | if (result.Succeeded) 51 | { 52 | await this.signInManager.SignInAsync(user, authenticationMethod: "AccountSecurityScheme", isPersistent: true); 53 | logger.LogInformation(3, "User created a new account with password."); 54 | return user; 55 | } else { 56 | AddErrors(result); 57 | } 58 | } 59 | 60 | return BadRequest(ModelState); 61 | } 62 | 63 | [HttpPost("login")] 64 | [AllowAnonymous] 65 | public async Task> Login([FromBody]LoginViewModel model) 66 | { 67 | if (ModelState.IsValid) 68 | { 69 | var result = await this.signInManager.PasswordSignInAsync(model.UserName, model.Password, true, false); 70 | 71 | if (result.Succeeded) 72 | { 73 | var user = await userManager.FindByNameAsync(model.UserName); 74 | var claims = await userManager.GetClaimsAsync(user); 75 | 76 | logger.LogDebug("#########"); 77 | logger.LogDebug(JsonConvert.SerializeObject(claims)); 78 | 79 | await signInManager.SignOutAsync(); 80 | await this.signInManager.SignInAsync(user, authenticationMethod: "AccountSecurityScheme", isPersistent: true); 81 | 82 | logger.LogDebug(JsonConvert.SerializeObject(user)); 83 | 84 | return user; 85 | } else { 86 | return BadRequest(result); 87 | } 88 | 89 | } 90 | else 91 | { 92 | return BadRequest(ModelState); 93 | } 94 | } 95 | 96 | [HttpGet("logout")] 97 | public async Task logout() 98 | { 99 | await signInManager.SignOutAsync(); 100 | return RedirectToAction("index", "Home"); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Extensions/SessionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Newtonsoft.Json; 3 | 4 | namespace AccountSecurity.Extensions 5 | { 6 | public static class SessionExtensions 7 | { 8 | public static void Set(this ISession session, string key, T value) 9 | { 10 | session.SetString(key, JsonConvert.SerializeObject(value)); 11 | } 12 | 13 | public static T Get(this ISession session, string key) 14 | { 15 | var value = session.GetString(key); 16 | 17 | return value == null ? default(T) : 18 | JsonConvert.DeserializeObject(value); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Migrations/20181017215715_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using AccountSecurity.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace AccountSecurity.Migrations 11 | { 12 | [DbContext(typeof(AccountSecurityContext))] 13 | [Migration("20181017215715_InitialCreate")] 14 | partial class InitialCreate 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.4-rtm-31024") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("AccountSecurity.Models.ApplicationUser", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("AccessFailedCount"); 30 | 31 | b.Property("AuthyId"); 32 | 33 | b.Property("ConcurrencyStamp") 34 | .IsConcurrencyToken(); 35 | 36 | b.Property("CountryCode"); 37 | 38 | b.Property("Email") 39 | .HasMaxLength(256); 40 | 41 | b.Property("EmailConfirmed"); 42 | 43 | b.Property("LockoutEnabled"); 44 | 45 | b.Property("LockoutEnd"); 46 | 47 | b.Property("NormalizedEmail") 48 | .HasMaxLength(256); 49 | 50 | b.Property("NormalizedUserName") 51 | .HasMaxLength(256); 52 | 53 | b.Property("PasswordHash"); 54 | 55 | b.Property("PhoneNumber"); 56 | 57 | b.Property("PhoneNumberConfirmed"); 58 | 59 | b.Property("SecurityStamp"); 60 | 61 | b.Property("TwoFactorEnabled"); 62 | 63 | b.Property("UserName") 64 | .HasMaxLength(256); 65 | 66 | b.HasKey("Id"); 67 | 68 | b.HasIndex("NormalizedEmail") 69 | .HasName("EmailIndex"); 70 | 71 | b.HasIndex("NormalizedUserName") 72 | .IsUnique() 73 | .HasName("UserNameIndex") 74 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 75 | 76 | b.ToTable("AspNetUsers"); 77 | }); 78 | 79 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 80 | { 81 | b.Property("Id") 82 | .ValueGeneratedOnAdd(); 83 | 84 | b.Property("ConcurrencyStamp") 85 | .IsConcurrencyToken(); 86 | 87 | b.Property("Name") 88 | .HasMaxLength(256); 89 | 90 | b.Property("NormalizedName") 91 | .HasMaxLength(256); 92 | 93 | b.HasKey("Id"); 94 | 95 | b.HasIndex("NormalizedName") 96 | .IsUnique() 97 | .HasName("RoleNameIndex") 98 | .HasFilter("[NormalizedName] IS NOT NULL"); 99 | 100 | b.ToTable("AspNetRoles"); 101 | }); 102 | 103 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 104 | { 105 | b.Property("Id") 106 | .ValueGeneratedOnAdd() 107 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 108 | 109 | b.Property("ClaimType"); 110 | 111 | b.Property("ClaimValue"); 112 | 113 | b.Property("RoleId") 114 | .IsRequired(); 115 | 116 | b.HasKey("Id"); 117 | 118 | b.HasIndex("RoleId"); 119 | 120 | b.ToTable("AspNetRoleClaims"); 121 | }); 122 | 123 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 124 | { 125 | b.Property("Id") 126 | .ValueGeneratedOnAdd() 127 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 128 | 129 | b.Property("ClaimType"); 130 | 131 | b.Property("ClaimValue"); 132 | 133 | b.Property("UserId") 134 | .IsRequired(); 135 | 136 | b.HasKey("Id"); 137 | 138 | b.HasIndex("UserId"); 139 | 140 | b.ToTable("AspNetUserClaims"); 141 | }); 142 | 143 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 144 | { 145 | b.Property("LoginProvider") 146 | .HasMaxLength(128); 147 | 148 | b.Property("ProviderKey") 149 | .HasMaxLength(128); 150 | 151 | b.Property("ProviderDisplayName"); 152 | 153 | b.Property("UserId") 154 | .IsRequired(); 155 | 156 | b.HasKey("LoginProvider", "ProviderKey"); 157 | 158 | b.HasIndex("UserId"); 159 | 160 | b.ToTable("AspNetUserLogins"); 161 | }); 162 | 163 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 164 | { 165 | b.Property("UserId"); 166 | 167 | b.Property("RoleId"); 168 | 169 | b.HasKey("UserId", "RoleId"); 170 | 171 | b.HasIndex("RoleId"); 172 | 173 | b.ToTable("AspNetUserRoles"); 174 | }); 175 | 176 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 177 | { 178 | b.Property("UserId"); 179 | 180 | b.Property("LoginProvider") 181 | .HasMaxLength(128); 182 | 183 | b.Property("Name") 184 | .HasMaxLength(128); 185 | 186 | b.Property("Value"); 187 | 188 | b.HasKey("UserId", "LoginProvider", "Name"); 189 | 190 | b.ToTable("AspNetUserTokens"); 191 | }); 192 | 193 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 194 | { 195 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 196 | .WithMany() 197 | .HasForeignKey("RoleId") 198 | .OnDelete(DeleteBehavior.Cascade); 199 | }); 200 | 201 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 202 | { 203 | b.HasOne("AccountSecurity.Models.ApplicationUser") 204 | .WithMany() 205 | .HasForeignKey("UserId") 206 | .OnDelete(DeleteBehavior.Cascade); 207 | }); 208 | 209 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 210 | { 211 | b.HasOne("AccountSecurity.Models.ApplicationUser") 212 | .WithMany() 213 | .HasForeignKey("UserId") 214 | .OnDelete(DeleteBehavior.Cascade); 215 | }); 216 | 217 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 218 | { 219 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 220 | .WithMany() 221 | .HasForeignKey("RoleId") 222 | .OnDelete(DeleteBehavior.Cascade); 223 | 224 | b.HasOne("AccountSecurity.Models.ApplicationUser") 225 | .WithMany() 226 | .HasForeignKey("UserId") 227 | .OnDelete(DeleteBehavior.Cascade); 228 | }); 229 | 230 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 231 | { 232 | b.HasOne("AccountSecurity.Models.ApplicationUser") 233 | .WithMany() 234 | .HasForeignKey("UserId") 235 | .OnDelete(DeleteBehavior.Cascade); 236 | }); 237 | #pragma warning restore 612, 618 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/AccountSecurity/Migrations/20181017215715_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Metadata; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace AccountSecurity.Migrations 6 | { 7 | public partial class InitialCreate : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "AspNetRoles", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false), 16 | Name = table.Column(maxLength: 256, nullable: true), 17 | NormalizedName = table.Column(maxLength: 256, nullable: true), 18 | ConcurrencyStamp = table.Column(nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_AspNetRoles", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "AspNetUsers", 27 | columns: table => new 28 | { 29 | Id = table.Column(nullable: false), 30 | UserName = table.Column(maxLength: 256, nullable: true), 31 | NormalizedUserName = table.Column(maxLength: 256, nullable: true), 32 | Email = table.Column(maxLength: 256, nullable: true), 33 | NormalizedEmail = table.Column(maxLength: 256, nullable: true), 34 | EmailConfirmed = table.Column(nullable: false), 35 | PasswordHash = table.Column(nullable: true), 36 | SecurityStamp = table.Column(nullable: true), 37 | ConcurrencyStamp = table.Column(nullable: true), 38 | PhoneNumber = table.Column(nullable: true), 39 | PhoneNumberConfirmed = table.Column(nullable: false), 40 | TwoFactorEnabled = table.Column(nullable: false), 41 | LockoutEnd = table.Column(nullable: true), 42 | LockoutEnabled = table.Column(nullable: false), 43 | AccessFailedCount = table.Column(nullable: false), 44 | AuthyId = table.Column(nullable: true), 45 | CountryCode = table.Column(nullable: true) 46 | }, 47 | constraints: table => 48 | { 49 | table.PrimaryKey("PK_AspNetUsers", x => x.Id); 50 | }); 51 | 52 | migrationBuilder.CreateTable( 53 | name: "AspNetRoleClaims", 54 | columns: table => new 55 | { 56 | Id = table.Column(nullable: false) 57 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 58 | RoleId = table.Column(nullable: false), 59 | ClaimType = table.Column(nullable: true), 60 | ClaimValue = table.Column(nullable: true) 61 | }, 62 | constraints: table => 63 | { 64 | table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); 65 | table.ForeignKey( 66 | name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", 67 | column: x => x.RoleId, 68 | principalTable: "AspNetRoles", 69 | principalColumn: "Id", 70 | onDelete: ReferentialAction.Cascade); 71 | }); 72 | 73 | migrationBuilder.CreateTable( 74 | name: "AspNetUserClaims", 75 | columns: table => new 76 | { 77 | Id = table.Column(nullable: false) 78 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 79 | UserId = table.Column(nullable: false), 80 | ClaimType = table.Column(nullable: true), 81 | ClaimValue = table.Column(nullable: true) 82 | }, 83 | constraints: table => 84 | { 85 | table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); 86 | table.ForeignKey( 87 | name: "FK_AspNetUserClaims_AspNetUsers_UserId", 88 | column: x => x.UserId, 89 | principalTable: "AspNetUsers", 90 | principalColumn: "Id", 91 | onDelete: ReferentialAction.Cascade); 92 | }); 93 | 94 | migrationBuilder.CreateTable( 95 | name: "AspNetUserLogins", 96 | columns: table => new 97 | { 98 | LoginProvider = table.Column(maxLength: 128, nullable: false), 99 | ProviderKey = table.Column(maxLength: 128, nullable: false), 100 | ProviderDisplayName = table.Column(nullable: true), 101 | UserId = table.Column(nullable: false) 102 | }, 103 | constraints: table => 104 | { 105 | table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); 106 | table.ForeignKey( 107 | name: "FK_AspNetUserLogins_AspNetUsers_UserId", 108 | column: x => x.UserId, 109 | principalTable: "AspNetUsers", 110 | principalColumn: "Id", 111 | onDelete: ReferentialAction.Cascade); 112 | }); 113 | 114 | migrationBuilder.CreateTable( 115 | name: "AspNetUserRoles", 116 | columns: table => new 117 | { 118 | UserId = table.Column(nullable: false), 119 | RoleId = table.Column(nullable: false) 120 | }, 121 | constraints: table => 122 | { 123 | table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); 124 | table.ForeignKey( 125 | name: "FK_AspNetUserRoles_AspNetRoles_RoleId", 126 | column: x => x.RoleId, 127 | principalTable: "AspNetRoles", 128 | principalColumn: "Id", 129 | onDelete: ReferentialAction.Cascade); 130 | table.ForeignKey( 131 | name: "FK_AspNetUserRoles_AspNetUsers_UserId", 132 | column: x => x.UserId, 133 | principalTable: "AspNetUsers", 134 | principalColumn: "Id", 135 | onDelete: ReferentialAction.Cascade); 136 | }); 137 | 138 | migrationBuilder.CreateTable( 139 | name: "AspNetUserTokens", 140 | columns: table => new 141 | { 142 | UserId = table.Column(nullable: false), 143 | LoginProvider = table.Column(maxLength: 128, nullable: false), 144 | Name = table.Column(maxLength: 128, nullable: false), 145 | Value = table.Column(nullable: true) 146 | }, 147 | constraints: table => 148 | { 149 | table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); 150 | table.ForeignKey( 151 | name: "FK_AspNetUserTokens_AspNetUsers_UserId", 152 | column: x => x.UserId, 153 | principalTable: "AspNetUsers", 154 | principalColumn: "Id", 155 | onDelete: ReferentialAction.Cascade); 156 | }); 157 | 158 | migrationBuilder.CreateIndex( 159 | name: "IX_AspNetRoleClaims_RoleId", 160 | table: "AspNetRoleClaims", 161 | column: "RoleId"); 162 | 163 | migrationBuilder.CreateIndex( 164 | name: "RoleNameIndex", 165 | table: "AspNetRoles", 166 | column: "NormalizedName", 167 | unique: true, 168 | filter: "[NormalizedName] IS NOT NULL"); 169 | 170 | migrationBuilder.CreateIndex( 171 | name: "IX_AspNetUserClaims_UserId", 172 | table: "AspNetUserClaims", 173 | column: "UserId"); 174 | 175 | migrationBuilder.CreateIndex( 176 | name: "IX_AspNetUserLogins_UserId", 177 | table: "AspNetUserLogins", 178 | column: "UserId"); 179 | 180 | migrationBuilder.CreateIndex( 181 | name: "IX_AspNetUserRoles_RoleId", 182 | table: "AspNetUserRoles", 183 | column: "RoleId"); 184 | 185 | migrationBuilder.CreateIndex( 186 | name: "EmailIndex", 187 | table: "AspNetUsers", 188 | column: "NormalizedEmail"); 189 | 190 | migrationBuilder.CreateIndex( 191 | name: "UserNameIndex", 192 | table: "AspNetUsers", 193 | column: "NormalizedUserName", 194 | unique: true, 195 | filter: "[NormalizedUserName] IS NOT NULL"); 196 | } 197 | 198 | protected override void Down(MigrationBuilder migrationBuilder) 199 | { 200 | migrationBuilder.DropTable( 201 | name: "AspNetRoleClaims"); 202 | 203 | migrationBuilder.DropTable( 204 | name: "AspNetUserClaims"); 205 | 206 | migrationBuilder.DropTable( 207 | name: "AspNetUserLogins"); 208 | 209 | migrationBuilder.DropTable( 210 | name: "AspNetUserRoles"); 211 | 212 | migrationBuilder.DropTable( 213 | name: "AspNetUserTokens"); 214 | 215 | migrationBuilder.DropTable( 216 | name: "AspNetRoles"); 217 | 218 | migrationBuilder.DropTable( 219 | name: "AspNetUsers"); 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/AccountSecurity/Migrations/AccountSecurityContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using AccountSecurity.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace AccountSecurity.Migrations 10 | { 11 | [DbContext(typeof(AccountSecurityContext))] 12 | partial class AccountSecurityContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.1.4-rtm-31024") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("AccountSecurity.Models.ApplicationUser", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("AccessFailedCount"); 28 | 29 | b.Property("AuthyId"); 30 | 31 | b.Property("ConcurrencyStamp") 32 | .IsConcurrencyToken(); 33 | 34 | b.Property("CountryCode"); 35 | 36 | b.Property("Email") 37 | .HasMaxLength(256); 38 | 39 | b.Property("EmailConfirmed"); 40 | 41 | b.Property("LockoutEnabled"); 42 | 43 | b.Property("LockoutEnd"); 44 | 45 | b.Property("NormalizedEmail") 46 | .HasMaxLength(256); 47 | 48 | b.Property("NormalizedUserName") 49 | .HasMaxLength(256); 50 | 51 | b.Property("PasswordHash"); 52 | 53 | b.Property("PhoneNumber"); 54 | 55 | b.Property("PhoneNumberConfirmed"); 56 | 57 | b.Property("SecurityStamp"); 58 | 59 | b.Property("TwoFactorEnabled"); 60 | 61 | b.Property("UserName") 62 | .HasMaxLength(256); 63 | 64 | b.HasKey("Id"); 65 | 66 | b.HasIndex("NormalizedEmail") 67 | .HasName("EmailIndex"); 68 | 69 | b.HasIndex("NormalizedUserName") 70 | .IsUnique() 71 | .HasName("UserNameIndex") 72 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 73 | 74 | b.ToTable("AspNetUsers"); 75 | }); 76 | 77 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 78 | { 79 | b.Property("Id") 80 | .ValueGeneratedOnAdd(); 81 | 82 | b.Property("ConcurrencyStamp") 83 | .IsConcurrencyToken(); 84 | 85 | b.Property("Name") 86 | .HasMaxLength(256); 87 | 88 | b.Property("NormalizedName") 89 | .HasMaxLength(256); 90 | 91 | b.HasKey("Id"); 92 | 93 | b.HasIndex("NormalizedName") 94 | .IsUnique() 95 | .HasName("RoleNameIndex") 96 | .HasFilter("[NormalizedName] IS NOT NULL"); 97 | 98 | b.ToTable("AspNetRoles"); 99 | }); 100 | 101 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 102 | { 103 | b.Property("Id") 104 | .ValueGeneratedOnAdd() 105 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 106 | 107 | b.Property("ClaimType"); 108 | 109 | b.Property("ClaimValue"); 110 | 111 | b.Property("RoleId") 112 | .IsRequired(); 113 | 114 | b.HasKey("Id"); 115 | 116 | b.HasIndex("RoleId"); 117 | 118 | b.ToTable("AspNetRoleClaims"); 119 | }); 120 | 121 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 122 | { 123 | b.Property("Id") 124 | .ValueGeneratedOnAdd() 125 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 126 | 127 | b.Property("ClaimType"); 128 | 129 | b.Property("ClaimValue"); 130 | 131 | b.Property("UserId") 132 | .IsRequired(); 133 | 134 | b.HasKey("Id"); 135 | 136 | b.HasIndex("UserId"); 137 | 138 | b.ToTable("AspNetUserClaims"); 139 | }); 140 | 141 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 142 | { 143 | b.Property("LoginProvider") 144 | .HasMaxLength(128); 145 | 146 | b.Property("ProviderKey") 147 | .HasMaxLength(128); 148 | 149 | b.Property("ProviderDisplayName"); 150 | 151 | b.Property("UserId") 152 | .IsRequired(); 153 | 154 | b.HasKey("LoginProvider", "ProviderKey"); 155 | 156 | b.HasIndex("UserId"); 157 | 158 | b.ToTable("AspNetUserLogins"); 159 | }); 160 | 161 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 162 | { 163 | b.Property("UserId"); 164 | 165 | b.Property("RoleId"); 166 | 167 | b.HasKey("UserId", "RoleId"); 168 | 169 | b.HasIndex("RoleId"); 170 | 171 | b.ToTable("AspNetUserRoles"); 172 | }); 173 | 174 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 175 | { 176 | b.Property("UserId"); 177 | 178 | b.Property("LoginProvider") 179 | .HasMaxLength(128); 180 | 181 | b.Property("Name") 182 | .HasMaxLength(128); 183 | 184 | b.Property("Value"); 185 | 186 | b.HasKey("UserId", "LoginProvider", "Name"); 187 | 188 | b.ToTable("AspNetUserTokens"); 189 | }); 190 | 191 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 192 | { 193 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 194 | .WithMany() 195 | .HasForeignKey("RoleId") 196 | .OnDelete(DeleteBehavior.Cascade); 197 | }); 198 | 199 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 200 | { 201 | b.HasOne("AccountSecurity.Models.ApplicationUser") 202 | .WithMany() 203 | .HasForeignKey("UserId") 204 | .OnDelete(DeleteBehavior.Cascade); 205 | }); 206 | 207 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 208 | { 209 | b.HasOne("AccountSecurity.Models.ApplicationUser") 210 | .WithMany() 211 | .HasForeignKey("UserId") 212 | .OnDelete(DeleteBehavior.Cascade); 213 | }); 214 | 215 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 216 | { 217 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 218 | .WithMany() 219 | .HasForeignKey("RoleId") 220 | .OnDelete(DeleteBehavior.Cascade); 221 | 222 | b.HasOne("AccountSecurity.Models.ApplicationUser") 223 | .WithMany() 224 | .HasForeignKey("UserId") 225 | .OnDelete(DeleteBehavior.Cascade); 226 | }); 227 | 228 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 229 | { 230 | b.HasOne("AccountSecurity.Models.ApplicationUser") 231 | .WithMany() 232 | .HasForeignKey("UserId") 233 | .OnDelete(DeleteBehavior.Cascade); 234 | }); 235 | #pragma warning restore 612, 618 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/AccountSecurity/Models/AccountSecurityContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace AccountSecurity.Models 10 | { 11 | public class AccountSecurityContext : IdentityDbContext 12 | { 13 | public AccountSecurityContext(DbContextOptions options) 14 | : base(options) 15 | { 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder builder) 19 | { 20 | base.OnModelCreating(builder); 21 | // Customize the ASP.NET Core Identity model and override the defaults if needed. 22 | // For example, you can rename the ASP.NET Core Identity table names and more. 23 | // Add your customizations after calling base.OnModelCreating(builder); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AccountSecurity/Models/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace account_security_csharp.Models 2 | { 3 | public class AppSettings 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Models/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace AccountSecurity.Models 4 | { 5 | public class LoginViewModel 6 | { 7 | [Required] 8 | [DataType(DataType.Text)] 9 | public string UserName { get; set; } 10 | 11 | [Required] 12 | [DataType(DataType.Password)] 13 | public string Password { get; set; } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Models/PhoneVerificationRequestModel.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Converters; 2 | using Newtonsoft.Json; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace AccountSecurity.Models 6 | { 7 | public class PhoneVerificationRequestModel 8 | { 9 | public PhoneVerificationRequestModel() { 10 | this.via = Verification.SMS; 11 | } 12 | 13 | [Required] 14 | [StringLength(4, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)] 15 | public string CountryCode { get; set; } 16 | 17 | [Required] 18 | [StringLength(16, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 7)] 19 | public string PhoneNumber { get; set; } 20 | 21 | public Verification via { get; set; } 22 | } 23 | 24 | [JsonConverter(typeof(StringEnumConverter))] 25 | public enum Verification 26 | { 27 | SMS, 28 | CALL 29 | } 30 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Models/RegisterViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AccountSecurity.Models 8 | { 9 | public class RegisterViewModel 10 | { 11 | [Required] 12 | [StringLength(40, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 13 | public string UserName { get; set; } 14 | 15 | [Required] 16 | [EmailAddress] 17 | public string Email { get; set; } 18 | 19 | [Required] 20 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 21 | [DataType(DataType.Password)] 22 | public string Password { get; set; } 23 | 24 | [Required] 25 | public string CountryCode { get; set; } 26 | 27 | [Required] 28 | [StringLength(20, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 9)] 29 | public string PhoneNumber { get; set; } 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Models/TokenVerificationModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace AccountSecurity.Models 4 | { 5 | public class TokenVerificationModel 6 | { 7 | [Required] 8 | [StringLength(8, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 4)] 9 | public string Token { get; set; } 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Models/applicationUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | 7 | namespace AccountSecurity.Models 8 | { 9 | public class ApplicationUser : IdentityUser 10 | { 11 | public string AuthyId { get; internal set; } 12 | public string CountryCode { get; internal set; } 13 | } 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/AccountSecurity/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace AccountSecurity 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | CreateWebHostBuilder(args) 19 | .ConfigureAppConfiguration((hostContext, config) => { 20 | config.AddCommandLine(args); 21 | }) 22 | .Build().Run(); 23 | } 24 | 25 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 26 | WebHost.CreateDefaultBuilder(args) 27 | .UseContentRoot(Directory.GetCurrentDirectory()) 28 | .UseKestrel() 29 | .UseStartup(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Services/AuthMessageSenderOptions.cs: -------------------------------------------------------------------------------- 1 | namespace AccountSecurity.Services 2 | { 3 | 4 | public class AuthMessageSenderOptions 5 | { 6 | public string SendGridUser { get; set; } 7 | public string SendGridKey { get; set; } 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/AccountSecurity/Services/Authy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using AccountSecurity.Models; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Logging; 9 | using Newtonsoft.Json; 10 | using Newtonsoft.Json.Linq; 11 | 12 | 13 | namespace AccountSecurity.Services 14 | { 15 | public interface IAuthy 16 | { 17 | Task registerUserAsync(RegisterViewModel user); 18 | Task verifyTokenAsync(string authyId, string token); 19 | Task verifyPhoneTokenAsync(string phoneNumber, string countryCode, string token); 20 | Task sendSmsAsync(string authyId); 21 | Task phoneVerificationCallRequestAsync(string countryCode, string phoneNumber); 22 | Task phoneVerificationRequestAsync(string countryCode, string phoneNumber); 23 | Task createApprovalRequestAsync(string authyId); 24 | Task checkRequestStatusAsync(string onetouch_uuid); 25 | } 26 | 27 | public class Authy : IAuthy 28 | { 29 | private readonly IConfiguration Configuration; 30 | private readonly IHttpClientFactory ClientFactory; 31 | private readonly ILogger logger; 32 | private readonly HttpClient client; 33 | 34 | public string message { get; private set; } 35 | 36 | public Authy(IConfiguration config, IHttpClientFactory clientFactory, ILoggerFactory loggerFactory) 37 | { 38 | Configuration = config; 39 | logger = loggerFactory.CreateLogger(); 40 | 41 | ClientFactory = clientFactory; 42 | client = ClientFactory.CreateClient(); 43 | client.BaseAddress = new Uri("https://api.authy.com"); 44 | client.DefaultRequestHeaders.Add("Accept", "application/json"); 45 | client.DefaultRequestHeaders.Add("user-agent", "Twilio Account Security C# Sample"); 46 | 47 | // Get Authy API Key from Configuration 48 | client.DefaultRequestHeaders.Add("X-Authy-API-Key", Configuration["AuthyApiKey"]); 49 | } 50 | 51 | public async Task registerUserAsync(RegisterViewModel user) 52 | { 53 | var userRegData = new Dictionary() { 54 | { "email", user.Email }, 55 | { "country_code", user.CountryCode }, 56 | { "cellphone", user.PhoneNumber } 57 | }; 58 | var userRegRequestData = new Dictionary() { }; 59 | userRegRequestData.Add("user", userRegData); 60 | var encodedContent = new FormUrlEncodedContent(userRegData); 61 | 62 | var result = await client.PostAsJsonAsync("/protected/json/users/new", userRegRequestData); 63 | 64 | logger.LogDebug(result.Content.ReadAsStringAsync().Result); 65 | 66 | result.EnsureSuccessStatusCode(); 67 | 68 | var response = await result.Content.ReadAsAsync>(); 69 | 70 | return JObject.FromObject(response["user"])["id"].ToString(); 71 | } 72 | 73 | public async Task verifyTokenAsync(string authyId, string token) 74 | { 75 | var result = await client.GetAsync($"/protected/json/verify/{token}/{authyId}"); 76 | 77 | logger.LogDebug(result.ToString()); 78 | logger.LogDebug(result.Content.ReadAsStringAsync().Result); 79 | 80 | var message = await result.Content.ReadAsStringAsync(); 81 | 82 | if (result.StatusCode == HttpStatusCode.OK) 83 | { 84 | return new TokenVerificationResult(message); 85 | } 86 | 87 | return new TokenVerificationResult(message, false); 88 | } 89 | 90 | public async Task verifyPhoneTokenAsync(string phoneNumber, string countryCode, string token) 91 | { 92 | var result = await client.GetAsync( 93 | $"/protected/json/phones/verification/check?phone_number={phoneNumber}&country_code={countryCode}&verification_code={token}" 94 | ); 95 | 96 | logger.LogDebug(result.ToString()); 97 | logger.LogDebug(result.Content.ReadAsStringAsync().Result); 98 | 99 | var message = await result.Content.ReadAsStringAsync(); 100 | 101 | if (result.StatusCode == HttpStatusCode.OK) 102 | { 103 | return new TokenVerificationResult(message); 104 | } 105 | 106 | return new TokenVerificationResult(message, false); 107 | } 108 | 109 | public async Task sendSmsAsync(string authyId) 110 | { 111 | var result = await client.GetAsync($"/protected/json/sms/{authyId}?force=true"); 112 | 113 | logger.LogDebug(result.ToString()); 114 | 115 | result.EnsureSuccessStatusCode(); 116 | 117 | return await result.Content.ReadAsStringAsync(); 118 | } 119 | 120 | public async Task phoneVerificationCallRequestAsync(string countryCode, string phoneNumber) 121 | { 122 | var result = await client.PostAsync( 123 | $"/protected/json/phones/verification/start?via=call&country_code={countryCode}&phone_number={phoneNumber}", 124 | null 125 | ); 126 | 127 | var content = await result.Content.ReadAsStringAsync(); 128 | 129 | logger.LogDebug(result.ToString()); 130 | logger.LogDebug(content); 131 | 132 | result.EnsureSuccessStatusCode(); 133 | 134 | return await result.Content.ReadAsStringAsync(); 135 | } 136 | 137 | public async Task phoneVerificationRequestAsync(string countryCode, string phoneNumber) 138 | { 139 | var result = await client.PostAsync( 140 | $"/protected/json/phones/verification/start?via=sms&country_code={countryCode}&phone_number={phoneNumber}", 141 | null 142 | ); 143 | 144 | var content = await result.Content.ReadAsStringAsync(); 145 | 146 | logger.LogDebug(result.ToString()); 147 | logger.LogDebug(content); 148 | 149 | result.EnsureSuccessStatusCode(); 150 | 151 | return await result.Content.ReadAsStringAsync(); 152 | } 153 | 154 | public async Task createApprovalRequestAsync(string authyId) 155 | { 156 | var requestData = new Dictionary() { 157 | { "message", "OneTouch Approval Request" }, 158 | { "details", "My Message Details" }, 159 | { "seconds_to_expire", "300" } 160 | }; 161 | 162 | var result = await client.PostAsJsonAsync( 163 | $"/onetouch/json/users/{authyId}/approval_requests", 164 | requestData 165 | ); 166 | 167 | logger.LogDebug(result.ToString()); 168 | var str_content = await result.Content.ReadAsStringAsync(); 169 | logger.LogDebug(str_content); 170 | 171 | result.EnsureSuccessStatusCode(); 172 | 173 | var content = await result.Content.ReadAsAsync>(); 174 | var approval_request_data = (JObject)content["approval_request"]; 175 | 176 | return (string)approval_request_data["uuid"]; 177 | } 178 | 179 | public async Task checkRequestStatusAsync(string onetouch_uuid) 180 | { 181 | var result = await client.GetAsync($"/onetouch/json/approval_requests/{onetouch_uuid}"); 182 | logger.LogDebug(result.ToString()); 183 | var str_content = await result.Content.ReadAsStringAsync(); 184 | logger.LogDebug(str_content); 185 | 186 | result.EnsureSuccessStatusCode(); 187 | 188 | return await result.Content.ReadAsAsync(); 189 | } 190 | } 191 | 192 | public class TokenVerificationResult 193 | { 194 | public TokenVerificationResult(string message, bool succeeded = true) 195 | { 196 | this.Message = message; 197 | this.Succeeded = succeeded; 198 | } 199 | 200 | public bool Succeeded { get; set; } 201 | public string Message { get; set; } 202 | } 203 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Services/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.UI.Services; 2 | using Microsoft.Extensions.Options; 3 | using SendGrid; 4 | using SendGrid.Helpers.Mail; 5 | using System.Threading.Tasks; 6 | 7 | namespace AccountSecurity.Services 8 | { 9 | public class EmailSender : IEmailSender 10 | { 11 | public EmailSender(IOptions optionsAccessor) 12 | { 13 | Options = optionsAccessor.Value; 14 | } 15 | 16 | public AuthMessageSenderOptions Options { get; } //set only via Secret Manager 17 | 18 | public Task SendEmailAsync(string email, string subject, string message) 19 | { 20 | return Execute(Options.SendGridKey, subject, message, email); 21 | } 22 | 23 | public Task Execute(string apiKey, string subject, string message, string email) 24 | { 25 | var client = new SendGridClient(apiKey); 26 | var msg = new SendGridMessage() 27 | { 28 | From = new EmailAddress("Joe@contoso.com", "Joe Smith"), 29 | Subject = subject, 30 | PlainTextContent = message, 31 | HtmlContent = message 32 | }; 33 | msg.AddTo(new EmailAddress(email)); 34 | 35 | // Disable click tracking. 36 | // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html 37 | msg.TrackingSettings = new TrackingSettings 38 | { 39 | ClickTracking = new ClickTracking { Enable = false } 40 | }; 41 | 42 | return client.SendEmailAsync(msg); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Startup.cs: -------------------------------------------------------------------------------- 1 | using AccountSecurity.Models; 2 | using AccountSecurity.Services; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Identity.UI.Services; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | using Microsoft.Extensions.Logging; 15 | using Newtonsoft.Json.Serialization; 16 | using System.Security.Claims; 17 | using System; 18 | 19 | namespace AccountSecurity { 20 | public class Startup { 21 | public IConfiguration Configuration { get; } 22 | 23 | private readonly ILogger logger; 24 | 25 | public Startup(IWebHostEnvironment env, IConfiguration config, ILoggerFactory loggerFactory) { 26 | logger = loggerFactory.CreateLogger(); 27 | Configuration = config; 28 | } 29 | 30 | public void ConfigureServices(IServiceCollection services) { 31 | services.Configure(options => 32 | { 33 | options.CheckConsentNeeded = context => false; 34 | options.MinimumSameSitePolicy = SameSiteMode.None; 35 | }); 36 | 37 | services.AddIdentityCore() 38 | .AddRoles() 39 | .AddEntityFrameworkStores() 40 | .AddSignInManager() 41 | .AddDefaultTokenProviders(); 42 | 43 | services.AddAuthentication(opts=>{ 44 | opts.DefaultScheme = IdentityConstants.ApplicationScheme; 45 | }) 46 | .AddIdentityCookies(opts =>{ 47 | opts.ApplicationCookie.Configure(c => { 48 | c.Cookie.Name = "MainCookie"; 49 | c.LoginPath = "/login"; 50 | c.LogoutPath = "/logout"; 51 | c.ExpireTimeSpan = TimeSpan.FromHours(1); 52 | }); 53 | }); 54 | 55 | services.AddDistributedMemoryCache(); 56 | 57 | services.AddSession(opts => { 58 | opts.Cookie.IsEssential = true; 59 | opts.IdleTimeout = TimeSpan.FromMinutes(1); 60 | opts.Cookie.HttpOnly = true; 61 | }); 62 | 63 | services.AddAuthorization(options => 64 | { 65 | options.DefaultPolicy = new AuthorizationPolicyBuilder() 66 | .RequireAuthenticatedUser() 67 | .Build(); 68 | 69 | options.AddPolicy("AuthyTwoFactor", policy => 70 | policy.RequireClaim("TokenVerification")); 71 | }); 72 | 73 | services.AddDbContext(options => 74 | options.UseSqlServer( 75 | Configuration.GetConnectionString("DefaultConnection"))); 76 | 77 | services.AddMvc() 78 | .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) 79 | .AddNewtonsoftJson(options => { 80 | options.SerializerSettings.ContractResolver = new DefaultContractResolver 81 | { 82 | NamingStrategy = new SnakeCaseNamingStrategy() 83 | }; 84 | }); 85 | 86 | services.AddHttpClient(); 87 | services.AddSingleton(); 88 | services.AddSingleton(); 89 | services.Configure(Configuration); 90 | } 91 | 92 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { 93 | if (env.IsDevelopment()) { 94 | app.UseDeveloperExceptionPage(); 95 | } else { 96 | app.UseExceptionHandler("/Error"); 97 | app.UseHsts(); 98 | } 99 | 100 | app.UseHttpsRedirection(); 101 | app.UseStaticFiles(); 102 | app.UseCookiePolicy(); 103 | app.UseSession(); 104 | app.UseRouting(); 105 | app.UseAuthentication(); 106 | app.UseAuthorization(); 107 | app.UseEndpoints(endpoints => 108 | { 109 | endpoints.MapRazorPages(); 110 | endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); 111 | }); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/AccountSecurity/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twilio Account Security Quickstart 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 |

Account Security Demos

38 |
39 |
40 |
41 |
42 |

43 | Implementations of both Twilio's Verify Phone Verification and Authy Two-Factor Authentication 44 |

45 | 52 |
53 |
54 |
55 |
56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /src/AccountSecurity/Views/Home/Login.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login - Twilio Authy Demo 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/AccountSecurity/Views/Home/Protected.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Protected Content - Twilio Authy Quickstart 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |

Protected by Twilio Authy 2FA

37 |
38 |
39 |
40 |
41 |

42 | Congrats! You have successfully implemented Twilio Authy Two-Factor Authentication. Browse the links below for more information related to Twilio Account Security. 43 |

44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/AccountSecurity/Views/Home/Register.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Register User - Account Security Quickstart 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 |
34 |

Quickstart Registration

35 |
36 |
37 |
38 |
39 |
40 |
41 | 45 |
46 |
47 |
48 | 49 |
50 | 53 |
54 | 55 |
56 |
57 |
58 | 62 |
63 |
64 |
65 |
66 | 70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 | 81 |
82 |
83 |
84 |
85 | 88 |
89 |
90 |
91 | 92 | 93 | Login 94 |
95 |
96 |
97 |
98 |
99 |
100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/AccountSecurity/Views/Home/TwoFactorSample.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twilio Authy Two-Factor Authentication Quickstart 6 | 7 | 8 | 10 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |

Token Verification

42 |
43 |
44 |
45 |
46 |
47 |
48 | 51 |
52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /src/AccountSecurity/Views/Home/Verification.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Start Phone Verification - Twilio Verify Quickstart 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 |
34 |

Twilio Verify

35 |
36 |
37 |
38 |
39 |
40 |
41 | 46 |
47 |
48 |
49 |
50 | 55 |
56 |
57 |
58 |
59 | 63 |
64 |
65 | 66 |
67 | 68 | 71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 83 |
84 |
85 |
86 |
87 | 90 |
91 |
92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/AccountSecurity/Views/Home/Verified.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Phone Verified Content - Account Security Quickstart 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |

Protected by Twilio Verify

37 |
38 |
39 |
40 |
41 |

42 | Congratulations. You have verified your Phone! 43 |

44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/AccountSecurity/appsettings.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Debug" 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Debug", 12 | "System": "Debug", 13 | "Microsoft": "Debug" 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/AccountSecurity/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "ConnectionStrings": { 9 | "DefaultConnection": "Server=127.0.0.1;Database=account_security;Trusted_Connection=True;MultipleActiveResultSets=true;Integrated Security=False;User ID=sa;Password=yourStrong(!)Password" 10 | }, 11 | "AuthyApiKey": "Your-Authy-Api-Key" 12 | } -------------------------------------------------------------------------------- /src/AccountSecurity/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "3.1.404" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/AccountSecurity/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "account-security-csharp", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ajv": { 8 | "version": "5.5.2", 9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", 10 | "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", 11 | "optional": true, 12 | "requires": { 13 | "co": "^4.6.0", 14 | "fast-deep-equal": "^1.0.0", 15 | "fast-json-stable-stringify": "^2.0.0", 16 | "json-schema-traverse": "^0.3.0" 17 | } 18 | }, 19 | "asn1": { 20 | "version": "0.2.4", 21 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 22 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 23 | "optional": true, 24 | "requires": { 25 | "safer-buffer": "~2.1.0" 26 | } 27 | }, 28 | "assert-plus": { 29 | "version": "1.0.0", 30 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 31 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 32 | }, 33 | "async": { 34 | "version": "1.0.0", 35 | "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", 36 | "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", 37 | "optional": true 38 | }, 39 | "asynckit": { 40 | "version": "0.4.0", 41 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 42 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 43 | "optional": true 44 | }, 45 | "aws-sign2": { 46 | "version": "0.7.0", 47 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 48 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 49 | "optional": true 50 | }, 51 | "aws4": { 52 | "version": "1.8.0", 53 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 54 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", 55 | "optional": true 56 | }, 57 | "balanced-match": { 58 | "version": "1.0.0", 59 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 60 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 61 | }, 62 | "bcrypt-pbkdf": { 63 | "version": "1.0.2", 64 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 65 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 66 | "optional": true, 67 | "requires": { 68 | "tweetnacl": "^0.14.3" 69 | } 70 | }, 71 | "brace-expansion": { 72 | "version": "1.1.11", 73 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 74 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 75 | "requires": { 76 | "balanced-match": "^1.0.0", 77 | "concat-map": "0.0.1" 78 | } 79 | }, 80 | "buffer-from": { 81 | "version": "1.1.1", 82 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 83 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 84 | "optional": true 85 | }, 86 | "caseless": { 87 | "version": "0.12.0", 88 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 89 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 90 | "optional": true 91 | }, 92 | "cli": { 93 | "version": "1.0.1", 94 | "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", 95 | "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", 96 | "requires": { 97 | "exit": "0.1.2", 98 | "glob": "^7.1.1" 99 | } 100 | }, 101 | "co": { 102 | "version": "4.6.0", 103 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 104 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", 105 | "optional": true 106 | }, 107 | "colors": { 108 | "version": "1.0.3", 109 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", 110 | "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", 111 | "optional": true 112 | }, 113 | "combined-stream": { 114 | "version": "1.0.6", 115 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 116 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 117 | "requires": { 118 | "delayed-stream": "~1.0.0" 119 | } 120 | }, 121 | "concat-map": { 122 | "version": "0.0.1", 123 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 124 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 125 | }, 126 | "concat-stream": { 127 | "version": "1.6.2", 128 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 129 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 130 | "optional": true, 131 | "requires": { 132 | "buffer-from": "^1.0.0", 133 | "inherits": "^2.0.3", 134 | "readable-stream": "^2.2.2", 135 | "typedarray": "^0.0.6" 136 | }, 137 | "dependencies": { 138 | "isarray": { 139 | "version": "1.0.0", 140 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 141 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 142 | "optional": true 143 | }, 144 | "readable-stream": { 145 | "version": "2.3.6", 146 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 147 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 148 | "optional": true, 149 | "requires": { 150 | "core-util-is": "~1.0.0", 151 | "inherits": "~2.0.3", 152 | "isarray": "~1.0.0", 153 | "process-nextick-args": "~2.0.0", 154 | "safe-buffer": "~5.1.1", 155 | "string_decoder": "~1.1.1", 156 | "util-deprecate": "~1.0.1" 157 | } 158 | }, 159 | "string_decoder": { 160 | "version": "1.1.1", 161 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 162 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 163 | "optional": true, 164 | "requires": { 165 | "safe-buffer": "~5.1.0" 166 | } 167 | } 168 | } 169 | }, 170 | "console-browserify": { 171 | "version": "1.1.0", 172 | "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", 173 | "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", 174 | "requires": { 175 | "date-now": "^0.1.4" 176 | } 177 | }, 178 | "core-util-is": { 179 | "version": "1.0.2", 180 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 181 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 182 | }, 183 | "cycle": { 184 | "version": "1.0.3", 185 | "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", 186 | "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", 187 | "optional": true 188 | }, 189 | "dashdash": { 190 | "version": "1.14.1", 191 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 192 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 193 | "optional": true, 194 | "requires": { 195 | "assert-plus": "^1.0.0" 196 | } 197 | }, 198 | "date-now": { 199 | "version": "0.1.4", 200 | "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", 201 | "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" 202 | }, 203 | "debug": { 204 | "version": "2.6.9", 205 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 206 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 207 | "optional": true, 208 | "requires": { 209 | "ms": "2.0.0" 210 | } 211 | }, 212 | "delayed-stream": { 213 | "version": "1.0.0", 214 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 215 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 216 | }, 217 | "dom-serializer": { 218 | "version": "0.1.0", 219 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", 220 | "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", 221 | "requires": { 222 | "domelementtype": "~1.1.1", 223 | "entities": "~1.1.1" 224 | }, 225 | "dependencies": { 226 | "domelementtype": { 227 | "version": "1.1.3", 228 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", 229 | "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" 230 | }, 231 | "entities": { 232 | "version": "1.1.1", 233 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", 234 | "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" 235 | } 236 | } 237 | }, 238 | "domelementtype": { 239 | "version": "1.3.0", 240 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", 241 | "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" 242 | }, 243 | "domhandler": { 244 | "version": "2.3.0", 245 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", 246 | "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", 247 | "requires": { 248 | "domelementtype": "1" 249 | } 250 | }, 251 | "domutils": { 252 | "version": "1.5.1", 253 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", 254 | "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", 255 | "requires": { 256 | "dom-serializer": "0", 257 | "domelementtype": "1" 258 | } 259 | }, 260 | "ecc-jsbn": { 261 | "version": "0.1.2", 262 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 263 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 264 | "optional": true, 265 | "requires": { 266 | "jsbn": "~0.1.0", 267 | "safer-buffer": "^2.1.0" 268 | } 269 | }, 270 | "entities": { 271 | "version": "1.0.0", 272 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", 273 | "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" 274 | }, 275 | "es6-promise": { 276 | "version": "4.2.5", 277 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", 278 | "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==", 279 | "optional": true 280 | }, 281 | "exit": { 282 | "version": "0.1.2", 283 | "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", 284 | "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" 285 | }, 286 | "extend": { 287 | "version": "3.0.2", 288 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 289 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 290 | "optional": true 291 | }, 292 | "extract-zip": { 293 | "version": "1.6.7", 294 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", 295 | "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", 296 | "optional": true, 297 | "requires": { 298 | "concat-stream": "1.6.2", 299 | "debug": "2.6.9", 300 | "mkdirp": "0.5.1", 301 | "yauzl": "2.4.1" 302 | } 303 | }, 304 | "extsprintf": { 305 | "version": "1.3.0", 306 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 307 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 308 | }, 309 | "eyes": { 310 | "version": "0.1.8", 311 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 312 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", 313 | "optional": true 314 | }, 315 | "fast-deep-equal": { 316 | "version": "1.1.0", 317 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", 318 | "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", 319 | "optional": true 320 | }, 321 | "fast-json-stable-stringify": { 322 | "version": "2.0.0", 323 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 324 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 325 | "optional": true 326 | }, 327 | "fd-slicer": { 328 | "version": "1.0.1", 329 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", 330 | "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", 331 | "optional": true, 332 | "requires": { 333 | "pend": "~1.2.0" 334 | } 335 | }, 336 | "forever-agent": { 337 | "version": "0.6.1", 338 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 339 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 340 | "optional": true 341 | }, 342 | "form-data": { 343 | "version": "2.3.2", 344 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 345 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 346 | "optional": true, 347 | "requires": { 348 | "asynckit": "^0.4.0", 349 | "combined-stream": "1.0.6", 350 | "mime-types": "^2.1.12" 351 | } 352 | }, 353 | "fs-extra": { 354 | "version": "1.0.0", 355 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", 356 | "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", 357 | "optional": true, 358 | "requires": { 359 | "graceful-fs": "^4.1.2", 360 | "jsonfile": "^2.1.0", 361 | "klaw": "^1.0.0" 362 | } 363 | }, 364 | "fs.realpath": { 365 | "version": "1.0.0", 366 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 367 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 368 | }, 369 | "getpass": { 370 | "version": "0.1.7", 371 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 372 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 373 | "optional": true, 374 | "requires": { 375 | "assert-plus": "^1.0.0" 376 | } 377 | }, 378 | "glob": { 379 | "version": "7.1.3", 380 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 381 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 382 | "requires": { 383 | "fs.realpath": "^1.0.0", 384 | "inflight": "^1.0.4", 385 | "inherits": "2", 386 | "minimatch": "^3.0.4", 387 | "once": "^1.3.0", 388 | "path-is-absolute": "^1.0.0" 389 | } 390 | }, 391 | "graceful-fs": { 392 | "version": "4.1.11", 393 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 394 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", 395 | "optional": true 396 | }, 397 | "har-schema": { 398 | "version": "2.0.0", 399 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 400 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 401 | "optional": true 402 | }, 403 | "har-validator": { 404 | "version": "5.1.0", 405 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", 406 | "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", 407 | "optional": true, 408 | "requires": { 409 | "ajv": "^5.3.0", 410 | "har-schema": "^2.0.0" 411 | } 412 | }, 413 | "hasha": { 414 | "version": "2.2.0", 415 | "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", 416 | "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", 417 | "optional": true, 418 | "requires": { 419 | "is-stream": "^1.0.1", 420 | "pinkie-promise": "^2.0.0" 421 | } 422 | }, 423 | "htmlparser2": { 424 | "version": "3.8.3", 425 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", 426 | "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", 427 | "requires": { 428 | "domelementtype": "1", 429 | "domhandler": "2.3", 430 | "domutils": "1.5", 431 | "entities": "1.0", 432 | "readable-stream": "1.1" 433 | } 434 | }, 435 | "http-signature": { 436 | "version": "1.2.0", 437 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 438 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 439 | "optional": true, 440 | "requires": { 441 | "assert-plus": "^1.0.0", 442 | "jsprim": "^1.2.2", 443 | "sshpk": "^1.7.0" 444 | } 445 | }, 446 | "inflight": { 447 | "version": "1.0.6", 448 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 449 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 450 | "requires": { 451 | "once": "^1.3.0", 452 | "wrappy": "1" 453 | } 454 | }, 455 | "inherits": { 456 | "version": "2.0.3", 457 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 458 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 459 | }, 460 | "is-stream": { 461 | "version": "1.1.0", 462 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 463 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", 464 | "optional": true 465 | }, 466 | "is-typedarray": { 467 | "version": "1.0.0", 468 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 469 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 470 | "optional": true 471 | }, 472 | "isarray": { 473 | "version": "0.0.1", 474 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 475 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 476 | }, 477 | "isexe": { 478 | "version": "2.0.0", 479 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 480 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 481 | "optional": true 482 | }, 483 | "isstream": { 484 | "version": "0.1.2", 485 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 486 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 487 | }, 488 | "jsbn": { 489 | "version": "0.1.1", 490 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 491 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 492 | "optional": true 493 | }, 494 | "jshint": { 495 | "version": "2.9.6", 496 | "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.6.tgz", 497 | "integrity": "sha512-KO9SIAKTlJQOM4lE64GQUtGBRpTOuvbrRrSZw3AhUxMNG266nX9hK2cKA4SBhXOj0irJGyNyGSLT62HGOVDEOA==", 498 | "requires": { 499 | "cli": "~1.0.0", 500 | "console-browserify": "1.1.x", 501 | "exit": "0.1.x", 502 | "htmlparser2": "3.8.x", 503 | "lodash": "~4.17.10", 504 | "minimatch": "~3.0.2", 505 | "phantom": "~4.0.1", 506 | "phantomjs-prebuilt": "~2.1.7", 507 | "shelljs": "0.3.x", 508 | "strip-json-comments": "1.0.x", 509 | "unicode-5.2.0": "^0.7.5" 510 | } 511 | }, 512 | "json-schema": { 513 | "version": "0.2.3", 514 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 515 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 516 | "optional": true 517 | }, 518 | "json-schema-traverse": { 519 | "version": "0.3.1", 520 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 521 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", 522 | "optional": true 523 | }, 524 | "json-stringify-safe": { 525 | "version": "5.0.1", 526 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 527 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 528 | "optional": true 529 | }, 530 | "jsonfile": { 531 | "version": "2.4.0", 532 | "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", 533 | "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", 534 | "optional": true, 535 | "requires": { 536 | "graceful-fs": "^4.1.6" 537 | } 538 | }, 539 | "jsprim": { 540 | "version": "1.4.1", 541 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 542 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 543 | "optional": true, 544 | "requires": { 545 | "assert-plus": "1.0.0", 546 | "extsprintf": "1.3.0", 547 | "json-schema": "0.2.3", 548 | "verror": "1.10.0" 549 | } 550 | }, 551 | "kew": { 552 | "version": "0.7.0", 553 | "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", 554 | "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", 555 | "optional": true 556 | }, 557 | "klaw": { 558 | "version": "1.3.1", 559 | "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", 560 | "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", 561 | "optional": true, 562 | "requires": { 563 | "graceful-fs": "^4.1.9" 564 | } 565 | }, 566 | "lodash": { 567 | "version": "4.17.11", 568 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 569 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" 570 | }, 571 | "mime-db": { 572 | "version": "1.36.0", 573 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", 574 | "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" 575 | }, 576 | "mime-types": { 577 | "version": "2.1.20", 578 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", 579 | "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", 580 | "requires": { 581 | "mime-db": "~1.36.0" 582 | } 583 | }, 584 | "minimatch": { 585 | "version": "3.0.4", 586 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 587 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 588 | "requires": { 589 | "brace-expansion": "^1.1.7" 590 | } 591 | }, 592 | "minimist": { 593 | "version": "0.0.8", 594 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 595 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 596 | "optional": true 597 | }, 598 | "mkdirp": { 599 | "version": "0.5.1", 600 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 601 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 602 | "optional": true, 603 | "requires": { 604 | "minimist": "0.0.8" 605 | } 606 | }, 607 | "ms": { 608 | "version": "2.0.0", 609 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 610 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 611 | "optional": true 612 | }, 613 | "oauth-sign": { 614 | "version": "0.9.0", 615 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 616 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 617 | "optional": true 618 | }, 619 | "once": { 620 | "version": "1.4.0", 621 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 622 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 623 | "requires": { 624 | "wrappy": "1" 625 | } 626 | }, 627 | "path-is-absolute": { 628 | "version": "1.0.1", 629 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 630 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 631 | }, 632 | "pend": { 633 | "version": "1.2.0", 634 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 635 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", 636 | "optional": true 637 | }, 638 | "performance-now": { 639 | "version": "2.1.0", 640 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 641 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 642 | "optional": true 643 | }, 644 | "phantom": { 645 | "version": "4.0.12", 646 | "resolved": "https://registry.npmjs.org/phantom/-/phantom-4.0.12.tgz", 647 | "integrity": "sha512-Tz82XhtPmwCk1FFPmecy7yRGZG2btpzY2KI9fcoPT7zT9det0CcMyfBFPp1S8DqzsnQnm8ZYEfdy528mwVtksA==", 648 | "optional": true, 649 | "requires": { 650 | "phantomjs-prebuilt": "^2.1.16", 651 | "split": "^1.0.1", 652 | "winston": "^2.4.0" 653 | } 654 | }, 655 | "phantomjs-prebuilt": { 656 | "version": "2.1.16", 657 | "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", 658 | "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=", 659 | "optional": true, 660 | "requires": { 661 | "es6-promise": "^4.0.3", 662 | "extract-zip": "^1.6.5", 663 | "fs-extra": "^1.0.0", 664 | "hasha": "^2.2.0", 665 | "kew": "^0.7.0", 666 | "progress": "^1.1.8", 667 | "request": "^2.81.0", 668 | "request-progress": "^2.0.1", 669 | "which": "^1.2.10" 670 | } 671 | }, 672 | "pinkie": { 673 | "version": "2.0.4", 674 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 675 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 676 | "optional": true 677 | }, 678 | "pinkie-promise": { 679 | "version": "2.0.1", 680 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 681 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 682 | "optional": true, 683 | "requires": { 684 | "pinkie": "^2.0.0" 685 | } 686 | }, 687 | "process-nextick-args": { 688 | "version": "2.0.0", 689 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 690 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", 691 | "optional": true 692 | }, 693 | "progress": { 694 | "version": "1.1.8", 695 | "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", 696 | "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", 697 | "optional": true 698 | }, 699 | "psl": { 700 | "version": "1.1.29", 701 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", 702 | "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", 703 | "optional": true 704 | }, 705 | "punycode": { 706 | "version": "1.4.1", 707 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 708 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", 709 | "optional": true 710 | }, 711 | "qs": { 712 | "version": "6.5.2", 713 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 714 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", 715 | "optional": true 716 | }, 717 | "readable-stream": { 718 | "version": "1.1.14", 719 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 720 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 721 | "requires": { 722 | "core-util-is": "~1.0.0", 723 | "inherits": "~2.0.1", 724 | "isarray": "0.0.1", 725 | "string_decoder": "~0.10.x" 726 | } 727 | }, 728 | "request": { 729 | "version": "2.88.0", 730 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 731 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 732 | "optional": true, 733 | "requires": { 734 | "aws-sign2": "~0.7.0", 735 | "aws4": "^1.8.0", 736 | "caseless": "~0.12.0", 737 | "combined-stream": "~1.0.6", 738 | "extend": "~3.0.2", 739 | "forever-agent": "~0.6.1", 740 | "form-data": "~2.3.2", 741 | "har-validator": "~5.1.0", 742 | "http-signature": "~1.2.0", 743 | "is-typedarray": "~1.0.0", 744 | "isstream": "~0.1.2", 745 | "json-stringify-safe": "~5.0.1", 746 | "mime-types": "~2.1.19", 747 | "oauth-sign": "~0.9.0", 748 | "performance-now": "^2.1.0", 749 | "qs": "~6.5.2", 750 | "safe-buffer": "^5.1.2", 751 | "tough-cookie": "~2.4.3", 752 | "tunnel-agent": "^0.6.0", 753 | "uuid": "^3.3.2" 754 | } 755 | }, 756 | "request-progress": { 757 | "version": "2.0.1", 758 | "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", 759 | "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", 760 | "optional": true, 761 | "requires": { 762 | "throttleit": "^1.0.0" 763 | } 764 | }, 765 | "safe-buffer": { 766 | "version": "5.1.2", 767 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 768 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 769 | }, 770 | "safer-buffer": { 771 | "version": "2.1.2", 772 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 773 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 774 | }, 775 | "shelljs": { 776 | "version": "0.3.0", 777 | "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", 778 | "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" 779 | }, 780 | "split": { 781 | "version": "1.0.1", 782 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 783 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 784 | "optional": true, 785 | "requires": { 786 | "through": "2" 787 | } 788 | }, 789 | "sshpk": { 790 | "version": "1.14.2", 791 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", 792 | "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", 793 | "optional": true, 794 | "requires": { 795 | "asn1": "~0.2.3", 796 | "assert-plus": "^1.0.0", 797 | "bcrypt-pbkdf": "^1.0.0", 798 | "dashdash": "^1.12.0", 799 | "ecc-jsbn": "~0.1.1", 800 | "getpass": "^0.1.1", 801 | "jsbn": "~0.1.0", 802 | "safer-buffer": "^2.0.2", 803 | "tweetnacl": "~0.14.0" 804 | } 805 | }, 806 | "stack-trace": { 807 | "version": "0.0.10", 808 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 809 | "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", 810 | "optional": true 811 | }, 812 | "string_decoder": { 813 | "version": "0.10.31", 814 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 815 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 816 | }, 817 | "strip-json-comments": { 818 | "version": "1.0.4", 819 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", 820 | "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=" 821 | }, 822 | "throttleit": { 823 | "version": "1.0.0", 824 | "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", 825 | "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", 826 | "optional": true 827 | }, 828 | "through": { 829 | "version": "2.3.8", 830 | "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", 831 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 832 | "optional": true 833 | }, 834 | "tough-cookie": { 835 | "version": "2.4.3", 836 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 837 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 838 | "optional": true, 839 | "requires": { 840 | "psl": "^1.1.24", 841 | "punycode": "^1.4.1" 842 | } 843 | }, 844 | "tunnel-agent": { 845 | "version": "0.6.0", 846 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 847 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 848 | "optional": true, 849 | "requires": { 850 | "safe-buffer": "^5.0.1" 851 | } 852 | }, 853 | "tweetnacl": { 854 | "version": "0.14.5", 855 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 856 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 857 | "optional": true 858 | }, 859 | "typedarray": { 860 | "version": "0.0.6", 861 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 862 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 863 | "optional": true 864 | }, 865 | "unicode-5.2.0": { 866 | "version": "0.7.5", 867 | "resolved": "https://registry.npmjs.org/unicode-5.2.0/-/unicode-5.2.0-0.7.5.tgz", 868 | "integrity": "sha512-KVGLW1Bri30x00yv4HNM8kBxoqFXr0Sbo55735nvrlsx4PYBZol3UtoWgO492fSwmsetzPEZzy73rbU8OGXJcA==" 869 | }, 870 | "util-deprecate": { 871 | "version": "1.0.2", 872 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 873 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 874 | "optional": true 875 | }, 876 | "uuid": { 877 | "version": "3.3.2", 878 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 879 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", 880 | "optional": true 881 | }, 882 | "verror": { 883 | "version": "1.10.0", 884 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 885 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 886 | "optional": true, 887 | "requires": { 888 | "assert-plus": "^1.0.0", 889 | "core-util-is": "1.0.2", 890 | "extsprintf": "^1.2.0" 891 | } 892 | }, 893 | "which": { 894 | "version": "1.3.1", 895 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 896 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 897 | "optional": true, 898 | "requires": { 899 | "isexe": "^2.0.0" 900 | } 901 | }, 902 | "winston": { 903 | "version": "2.4.4", 904 | "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", 905 | "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==", 906 | "optional": true, 907 | "requires": { 908 | "async": "~1.0.0", 909 | "colors": "1.0.x", 910 | "cycle": "1.0.x", 911 | "eyes": "0.1.x", 912 | "isstream": "0.1.x", 913 | "stack-trace": "0.0.x" 914 | } 915 | }, 916 | "wrappy": { 917 | "version": "1.0.2", 918 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 919 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 920 | }, 921 | "yauzl": { 922 | "version": "2.4.1", 923 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", 924 | "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", 925 | "optional": true, 926 | "requires": { 927 | "fd-slicer": "~1.0.1" 928 | } 929 | } 930 | } 931 | } 932 | -------------------------------------------------------------------------------- /src/AccountSecurity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Account Security", 3 | "version": "1.0.0", 4 | "description": "Account Security with Twilio Authy in C#", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "jshint": "^2.9.6" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/AccountSecurity/wwwroot/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('accountSecurityQuickstart', []); 2 | 3 | app.controller('LoginController', function ($scope, $http, $window) { 4 | 5 | $scope.setup = {}; 6 | 7 | $scope.login = function () { 8 | $http.post('/api/user/login', $scope.setup) 9 | .success(function (data, status, headers, config) { 10 | console.log("Login success: ", data); 11 | $window.location.href = $window.location.origin + "/2fa"; 12 | }) 13 | .error(function (data, status, headers, config) { 14 | console.error("Login error: ", data); 15 | alert("Error logging in. Check console \n" + JSON.stringify(data)); 16 | }); 17 | }; 18 | }); 19 | 20 | 21 | app.controller('RegistrationController', function ($scope, $http, $window) { 22 | 23 | $scope.setup = {}; 24 | 25 | $scope.register = function () { 26 | if ($scope.password === $scope.password2 && $scope.password !== "") { 27 | $scope.setup.password = $scope.password; 28 | 29 | $http.post('/api/user/register', $scope.setup) 30 | .success(function (data, status, headers, config) { 31 | console.log("Success registering: ", data); 32 | $window.location.href = $window.location.origin + "/2fa"; 33 | }) 34 | .error(function (data, status, headers, config) { 35 | console.error("Registration error: ", data); 36 | alert("Error registering. Check console \n" + JSON.stringify(data)); 37 | }); 38 | } else { 39 | alert("Passwords do not match"); 40 | } 41 | }; 42 | }); 43 | 44 | app.controller('AuthyController', function ($scope, $http, $window, $interval) { 45 | 46 | var pollingID; 47 | 48 | $scope.setup = {}; 49 | 50 | $scope.logout = function () { 51 | $http.get('/api/user/logout') 52 | .success(function (data, status, headers, config) { 53 | console.log("Logout Response: ", data); 54 | $window.location.href = $window.location.origin + "/"; 55 | }) 56 | .error(function (data, status, headers, config) { 57 | console.error("Logout Error: ", data); 58 | }); 59 | }; 60 | 61 | /** 62 | * Request a token via SMS 63 | */ 64 | $scope.sms = function () { 65 | $http.post('/api/accountsecurity/sms') 66 | .success(function (data, status, headers, config) { 67 | console.log("SMS sent: ", data); 68 | }) 69 | .error(function (data, status, headers, config) { 70 | console.error("SMS error: ", data); 71 | alert("Problem sending SMS"); 72 | }); 73 | }; 74 | 75 | /** 76 | * Request a Voice delivered token 77 | */ 78 | $scope.voice = function () { 79 | $http.post('/api/accountsecurity/voice') 80 | .success(function (data, status, headers, config) { 81 | console.log("Phone call initialized: ", data); 82 | }) 83 | .error(function (data, status, headers, config) { 84 | console.error("Voice call error: ", data); 85 | alert("Problem making Voice Call"); 86 | }); 87 | }; 88 | 89 | /** 90 | * Verify a SMS, Voice or SoftToken 91 | */ 92 | $scope.verify = function () { 93 | $http.post('/api/accountsecurity/verify', {token: $scope.setup.token}) 94 | .success(function (data, status, headers, config) { 95 | console.log("2FA success ", data); 96 | $window.location.href = $window.location.origin + "/protected"; 97 | }) 98 | .error(function (data, status, headers, config) { 99 | console.error("Verify error: ", data); 100 | alert("Problem verifying token \n" + JSON.stringify(data)); 101 | }); 102 | }; 103 | 104 | /** 105 | * Request a OneTouch transaction 106 | */ 107 | $scope.onetouch = function () { 108 | $http.post('/api/accountsecurity/onetouch') 109 | .success(function (data, status, headers, config) { 110 | console.log("OneTouch success", data); 111 | /** 112 | * Poll for the status change. Every 5 seconds for 12 times. 1 minute. 113 | */ 114 | pollingID = $interval(oneTouchStatus, 5000, 12); 115 | }) 116 | .error(function (data, status, headers, config) { 117 | console.error("Onetouch error: ", data); 118 | alert("Problem creating OneTouch request \n" + JSON.stringify(data)); 119 | }); 120 | }; 121 | 122 | /** 123 | * Request the OneTouch status. 124 | */ 125 | function oneTouchStatus() { 126 | $http.post('/api/accountsecurity/onetouchstatus') 127 | .success(function (data, status, headers, config) { 128 | console.log("OneTouch Status: ", data); 129 | if (data.approval_request.status === "approved") { 130 | $window.location.href = $window.location.origin + "/protected"; 131 | $interval.cancel(pollingID); 132 | } else { 133 | console.log("One Touch Request not yet approved"); 134 | } 135 | }) 136 | .error(function (data, status, headers, config) { 137 | console.log("OneTouch Polling Status: ", data); 138 | alert("Something went wrong with the OneTouch polling \n" + JSON.stringify(data)); 139 | $interval.cancel(pollingID); 140 | }); 141 | } 142 | }); 143 | 144 | app.controller('PhoneVerificationController', function ($scope, $http, $window, $timeout) { 145 | 146 | $scope.setup = { 147 | via: "sms" 148 | }; 149 | 150 | $scope.view = { 151 | start: true 152 | }; 153 | 154 | /** 155 | * Initialize Phone Verification 156 | */ 157 | $scope.startVerification = function () { 158 | $http.post('/api/verification/start', $scope.setup) 159 | .success(function (data, status, headers, config) { 160 | $scope.view.start = false; 161 | console.log("Verification started: ", data); 162 | }) 163 | .error(function (data, status, headers, config) { 164 | console.error("Phone verification error: ", data); 165 | alert("Error verifying the token. Check console for details. \n" + JSON.stringify(data)); 166 | }); 167 | }; 168 | 169 | /** 170 | * Verify phone token 171 | */ 172 | $scope.verifyToken = function () { 173 | $http.post('/api/verification/verify', $scope.setup) 174 | .success(function (data, status, headers, config) { 175 | console.log("Phone Verification Success success: ", data); 176 | $window.location.href = $window.location.origin + "/verified"; 177 | }) 178 | .error(function (data, status, headers, config) { 179 | console.error("Verification error: ", data); 180 | alert("Error verifying the token. Check console for details.\n" + JSON.stringify(data)); 181 | }); 182 | }; 183 | 184 | $scope.logout = function () { 185 | $window.location.href = $window.location.origin; 186 | }; 187 | }); 188 | 189 | -------------------------------------------------------------------------------- /src/AccountSecurity/wwwroot/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/account-security-csharp/2608fcc4e4e35265ad76190392da9ecd9a373887/src/AccountSecurity/wwwroot/assets/favicon-16x16.png -------------------------------------------------------------------------------- /src/AccountSecurity/wwwroot/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/account-security-csharp/2608fcc4e4e35265ad76190392da9ecd9a373887/src/AccountSecurity/wwwroot/assets/favicon-32x32.png -------------------------------------------------------------------------------- /src/AccountSecurity/wwwroot/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/account-security-csharp/2608fcc4e4e35265ad76190392da9ecd9a373887/src/AccountSecurity/wwwroot/assets/favicon.ico -------------------------------------------------------------------------------- /src/AccountSecurity/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twilio Account Security Quickstart 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 |

Account Security Demos

38 |
39 |
40 |
41 |
42 |

43 | Implementations of both Twilio's Verify Phone Verification and Authy Two-Factor Authentication 44 |

45 | 52 |
53 |
54 |
55 |
56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | -------------------------------------------------------------------------------- /src/AccountSecurity/wwwroot/partials/links.html: -------------------------------------------------------------------------------- 1 | 8 | --------------------------------------------------------------------------------