We now support TOTP for logging in :D

This commit is contained in:
Muhammad Azeez 2021-01-17 23:02:50 +03:00
parent 516177f433
commit f993d6f0d0
11 changed files with 134 additions and 18 deletions

View File

@ -8,6 +8,8 @@
<PackageReference Include="IdentityServer4" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="TwoStepsAuthenticator" Version="1.4.1" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties properties_4launchsettings_1json__JsonSchema="https://json.schemastore.org/local.settings" /></VisualStudio></ProjectExtensions>

View File

@ -1,4 +1,4 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
@ -14,8 +14,12 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OidcSamples.AuthorizationServer.Quickstart.Account;
using System;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace IdentityServerHost.Quickstart.UI
@ -109,8 +113,21 @@ namespace IdentityServerHost.Quickstart.UI
if (ModelState.IsValid)
{
bool isValid = false;
if (model.UseTotp)
{
var user = _users.FindByUsername(model.Username);
var authenticator = new TwoStepsAuthenticator.TimeAuthenticator();
string secret = Convert.ToBase64String(Encoding.ASCII.GetBytes(user.Password));
isValid = authenticator.CheckCode(secret, model.Totp, user);
}
else
{
isValid = _users.ValidateCredentials(model.Username, model.Password);
}
// validate username/password against in-memory store
if (_users.ValidateCredentials(model.Username, model.Password))
if (isValid)
{
var user = _users.FindByUsername(model.Username);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId));
@ -164,7 +181,7 @@ namespace IdentityServerHost.Quickstart.UI
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.Client.ClientId));
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.Client.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
@ -173,7 +190,7 @@ namespace IdentityServerHost.Quickstart.UI
return View(vm);
}
/// <summary>
/// Show logout page
/// </summary>
@ -233,6 +250,30 @@ namespace IdentityServerHost.Quickstart.UI
return View();
}
private readonly char[] _chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
[HttpGet]
public IActionResult RegisterMobileDevice()
{
// NOTE: This is a dummy implemenatation and should NOT NEVER BE USED IN PROD!!!
var subject = User.FindFirst(JwtClaimTypes.Subject)?.Value;
var user = _users.FindBySubjectId(subject);
string secret = Convert.ToBase64String(Encoding.ASCII.GetBytes(user.Password));
var keyUri = string.Format(
"otpauth://totp/{0}:{1}?secret={2}&issuer={0}",
WebUtility.UrlEncode("KRG"),
WebUtility.UrlEncode(user.SubjectId),
secret);
return View(new TotpViewModel
{
Secret = secret,
KeyUri = keyUri
});
}
/*****************************************/
/* helper APIs for the AccountController */

View File

@ -10,8 +10,10 @@ namespace IdentityServerHost.Quickstart.UI
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
public string Totp { get; set; }
[Required]
public bool UseTotp { get; set; }
public bool RememberLogin { get; set; }
public string ReturnUrl { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace OidcSamples.AuthorizationServer.Quickstart.Account
{
public class TotpViewModel
{
public string KeyUri { get; set; }
public string Secret { get; set; }
}
}

View File

@ -29,7 +29,7 @@ namespace IdentityServerHost.Quickstart.UI
new Claim(JwtClaimTypes.FamilyName, "Azeez"),
new Claim(JwtClaimTypes.Email, "muhammad-azeez@outlook.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
}
},
}
};
}

View File

@ -67,6 +67,12 @@ namespace OidcSamples.AuthorizationServer
MinimumSameSitePolicy = SameSiteMode.Lax
});
app.Use(async (ctx, next) =>
{
ctx.Response.Headers.Add("Content-Security-Policy", new Microsoft.Extensions.Primitives.StringValues("default-src *; style-src 'self' http://* 'unsafe-inline'; script-src 'self' 'unsafe-inline' http://* 'unsafe-inline' 'unsafe-eval'"));
await next();
});
app.UseStaticFiles();
app.UseRouting();

View File

@ -26,21 +26,30 @@
<label asp-for="Username"></label>
<input class="form-control" placeholder="Username" asp-for="Username" autofocus>
</div>
<div class="form-group">
<label asp-for="UseTotp">Use Your mobile phone for authentication?</label>
<input asp-for="UseTotp" />
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input type="password" class="form-control" placeholder="Password" asp-for="Password" autocomplete="off">
</div>
<div class="form-group">
<label asp-for="Totp">TOTP</label>
<input type="password" class="form-control" placeholder="Type in the one-time-password from your authenticator app" asp-for="Totp" autocomplete="off">
</div>
@if (Model.AllowRememberLogin)
{
<div class="form-group">
<div class="form-check">
<input class="form-check-input" asp-for="RememberLogin">
<label class="form-check-label" asp-for="RememberLogin">
Remember My Login
</label>
</div>
</div>
}
<div class="form-group">
<div class="form-check">
<input class="form-check-input" asp-for="RememberLogin">
<label class="form-check-label" asp-for="RememberLogin">
Remember My Login
</label>
</div>
</div>}
<button class="btn btn-primary" name="button" value="login">Login</button>
<button class="btn btn-secondary" name="button" value="cancel">Cancel</button>
</form>

View File

@ -0,0 +1,45 @@
@model OidcSamples.AuthorizationServer.Quickstart.Account.TotpViewModel
<div>
<div class="page-header">
<h1>Register for MFA</h1>
</div>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Scan the QR code with your authenticator application</h3>
</div>
<div class="panel-body">
<form asp-route="RegisterForMfa">
<fieldset>
<input type="hidden" asp-for="Secret" />
<div class="form-group">
<div id="qrCode"></div>
</div>
<div class="form-group">
<button class="btn btn-primary">Registration successful</button>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script src="~/lib/qrcodejs/qrcode.min.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.KeyUri)",
width: 150,
height: 150
});
</script>
}

View File

@ -1,15 +1,16 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
<meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' http://* 'unsafe-inline'; script-src 'self' 'unsafe-inline' http://* 'unsafe-inline' 'unsafe-eval'" />
<title>IdentityServer4</title>
<link rel="icon" type="image/x-icon" href="~/favicon.ico" />
<link rel="shortcut icon" type="image/x-icon" href="~/favicon.ico" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>

View File

@ -23,6 +23,7 @@
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">@name <b class="caret"></b></a>
<div class="dropdown-menu">
<a class="dropdown-item" asp-action="RegisterMobileDevice" asp-controller="Account">RegisterMobileDevice</a>
<a class="dropdown-item" asp-action="Logout" asp-controller="Account">Logout</a>
</div>
</li>

File diff suppressed because one or more lines are too long