到目前为止,您已经学习了如何使用Oauth2处理程序,但是您会注意到,对于每个请求,您都需要进行身份验证。这是因为处理程序没有状态,并且在示例中没有应用状态管理。
尽管对于面向API的端点建议没有状态,例如,对于面向用户的端点使用JWT(我们稍后将介绍这些),我们可以将身份验证结果存储在会话中。为了实现这一点,我们需要一个类似以下代码段的应用程序:
OAuth2Auth authProvider =
GithubAuth
.create(vertx, "CLIENTID", "CLIENT SECRET");
// We need a user session handler too to make sure
// the user is stored in the session between requests
router.route()
.handler(SessionHandler.create(LocalSessionStore.create(vertx)));
// we now protect the resource under the path "/protected"
router.route("/protected").handler(
OAuth2AuthHandler.create(
vertx,
authProvider,
"http://localhost:8080/callback")
// we now configure the oauth2 handler, it will
// setup the callback handler
// as expected by your oauth2 provider.
.setupCallback(router.route("/callback"))
// for this resource we require that users have
// the authority to retrieve the user emails
.withScope("user:email")
);
// Entry point to the application, this will render
// a custom template.
router.get("/").handler(ctx -> ctx.response()
.putHeader("Content-Type", "text/html")
.end(
"<html>\n" +
" <body>\n" +
" <p>\n" +
" Well, hello there!\n" +
" </p>\n" +
" <p>\n" +
" We're going to the protected resource, if there is no\n" +
" user in the session we will talk to the GitHub API. Ready?\n" +
" <a href=\"/protected\">Click here</a> to begin!</a>\n" +
" </p>\n" +
" <p>\n" +
" <b>If that link doesn't work</b>, remember to provide your\n" +
" own <a href=\"https://github.com/settings/applications/new\">\n" +
" Client ID</a>!\n" +
" </p>\n" +
" </body>\n" +
"</html>"));
// The protected resource
router.get("/protected").handler(ctx -> {
// at this moment your user object should contain the info
// from the Oauth2 response, since this is a protected resource
// as specified above in the handler config the user object is never null
User user = ctx.user();
// just dump it to the client for demo purposes
ctx.response().end(user.toString());
});
Oauth2与JWT混用
一个提供者使用jwt tokens作为访问token,在想混合基于客户端认证和API认证,这是符合RFC6740标准且十分有用的。例如说,你有一个应用,想给一些HTML文档一些保护,但是也想这些页面被API消费。在这种情况下,API不能方便执行OAthus2要求的转发握手请求。倮可以在握手前使用提供的token.
只要提供配置支持JWT,这个处理会被处理器自动处理。
在现实中指的是,你的API可以通过Authorization头设为"Bearer BASE64_ACCESS_TOKEN"可以访问你保护的资源。
WebAuthn
我们的在线生活依赖于过时而脆弱的密码观念。密码是恶意用户与您的银行帐户或社交媒体帐户之间的密码。密码难以维护;很难将它们存储在服务器上(密码被盗)。它们很难记住,或者不告诉别人(网络钓鱼攻击)。
但还有更好的办法!一个无密码的世界,它是W3C和FIDO联盟在您的浏览器上运行的标准。
WebAuthn是一种API,它允许服务器使用公钥密码而不是密码来注册和验证用户,这是一种在身份验证设备(例如yubikey令牌或手机)的帮助下以用户可访问的方式使用密码的API。
该协议要求至少在路由器上安装第一个回调:
-
/webauthn/response
执行所有验证的回调URL -
/webauthn/login
允许用户开始登录流程的URL(可选的,但是没有此URL将不能登录) -
/webauthn/register
允许用户注册一个新身份的URL(可选,如果注册数据已经被存储,可以不需要)
一个保护应用程序的例子:
WebAuthn webAuthn = WebAuthn.create(
vertx,
new WebAuthnOptions()
.setRelyingParty(new RelyingParty().setName("Vert.x WebAuthN Demo"))
// What kind of authentication do you want? do you care?
// # security keys
.setAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
// # fingerprint
.setAuthenticatorAttachment(AuthenticatorAttachment.PLATFORM)
.setUserVerification(UserVerification.REQUIRED))
// where to load the credentials from?
.authenticatorFetcher(fetcher)
// update the state of an authenticator
.authenticatorUpdater(updater);
// parse the BODY
router.post()
.handler(BodyHandler.create());
// add a session handler
router.route()
.handler(SessionHandler
.create(LocalSessionStore.create(vertx)));
// security handler
WebAuthnHandler webAuthNHandler = WebAuthnHandler.create(webAuthn)
.setOrigin("https://192.168.178.74.xip.io:8443")
// required callback
.setupCallback(router.post("/webauthn/response"))
// optional register callback
.setupCredentialsCreateCallback(router.post("/webauthn/register"))
// optional login callback
.setupCredentialsGetCallback(router.post("/webauthn/login"));
// secure the remaining routes
router.route().handler(webAuthNHandler);
这个应用程在后端是不安全的,但是有一些代码需要在客户端执行。需要一些样板代码,使用以下两个功能:
/**
* Converts PublicKeyCredential into serialised JSON
* @param {Object} pubKeyCred
* @return {Object} - JSON encoded publicKeyCredential
*/
var publicKeyCredentialToJSON = (pubKeyCred) => {
if (pubKeyCred instanceof Array) {
let arr = [];
for (let i of pubKeyCred) { arr.push(publicKeyCredentialToJSON(i)) }
return arr
}
if (pubKeyCred instanceof ArrayBuffer) {
return base64url.encode(pubKeyCred)
}
if (pubKeyCred instanceof Object) {
let obj = {};
for (let key in pubKeyCred) {
obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
}
return obj
}
return pubKeyCred
};
/**
* Generate secure random buffer
* @param {Number} len - Length of the buffer (default 32 bytes)
* @return {Uint8Array} - random string
*/
var generateRandomBuffer = (len) => {
len = len || 32;
let randomBuffer = new Uint8Array(len);
window.crypto.getRandomValues(randomBuffer);
return randomBuffer
};
/**
* Decodes arrayBuffer required fields.
*/
var preformatMakeCredReq = (makeCredReq) => {
makeCredReq.challenge = base64url.decode(makeCredReq.challenge);
makeCredReq.user.id = base64url.decode(makeCredReq.user.id);
return makeCredReq
};
/**
* Decodes arrayBuffer required fields.
*/
var preformatGetAssertReq = (getAssert) => {
getAssert.challenge = base64url.decode(getAssert.challenge);
for (let allowCred of getAssert.allowCredentials) {
allowCred.id = base64url.decode(allowCred.id)
}
return getAssert
};
这些函数将帮助你与服务器交互,不需要更多东西。让我们用一个用户登录:
// using the functions defined before...
getGetAssertionChallenge({name: 'your-user-name'})
.then((response) => {
// base64 must be decoded to a JavaScript Buffer
let publicKey = preformatGetAssertReq(response);
// the response is then passed to the browser
// to generate an assertion by interacting with your token/phone/etc...
return navigator.credentials.get({publicKey})
})
.then((response) => {
// convert response buffers to base64 and json
let getAssertionResponse = publicKeyCredentialToJSON(response);
// send information to server
return sendWebAuthnResponse(getAssertionResponse)
})
.then((response) => {
// success!
alert('Login success')
})
.catch((error) => alert(error));
// utility functions
let sendWebAuthnResponse = (body) => {
return fetch('/webauthn/response', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
.then(response => {
if (!response.ok) {
throw new Error(`Server responded with error: ${response.statusText}`);
}
return response;
})
};
let getGetAssertionChallenge = (formBody) => {
return fetch('/webauthn/login', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formBody)
})
.then(response => {
if (!response.ok) {
throw new Error(`Server responded with error: ${response.statusText}`);
}
return response;
})
.then((response) => response.json())
};
上面的示例已经涵盖了66%的API,其中包括3个端点中的2个。最后一个端点是用户注册。用户注册是将新密钥注册到服务器凭据存储并映射到用户的过程,当然,在客户端创建了一个私钥并与服务器关联,但该密钥从未离开硬件令牌或您的手机安全芯片。
要注册用户并重用上面已经定义的大多数功能,请执行以下操作:
/* Handle for register form submission */
getMakeCredentialsChallenge({name: 'myalias', displayName: 'Paulo Lopes'})
.then((response) => {
// convert challenge & id to buffer and perform register
let publicKey = preformatMakeCredReq(response);
// create a new secure key pair
return navigator.credentials.create({publicKey})
})
.then((response) => {
// convert response from buffer to json
let makeCredResponse = window.publicKeyCredentialToJSON(response);
// send to server to confirm the user
return sendWebAuthnResponse(makeCredResponse)
})
.then((response) => {
alert('Registration completed')
})
.catch((error) => alert(error));
// utility functions
let getMakeCredentialsChallenge = (formBody) => {
return fetch('/webauthn/register', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formBody)
})
.then(response => {
if (!response.ok) {
throw new Error(`Server responded with error: ${response.statusText}`);
}
return response;
})
.then((response) => response.json())
};
警告
由于API的安全性,浏览器将不允许您在纯文本HTTP上使用此API。所有请求都必须通过HTTPS。
警告
WebAuthN需要HTTPS和有效的TLS证书,您也可以在开发期间使用自签名证书。
一次性密码(Multi-Factor Authentication)
Vert.x还支持多因素身份验证。使用MFA有两个选项:
- HOTP-基于哈希的一次性密码
- TOTP-基于时间的一次性密码
提供程序之间的用法是相同的,因此存在一个处理程序,允许您在构造函数级别选择所需的模式。
此处理程序的行为如下:
如果在请求中没有用户,则假设没有执行先前的认证。这意味着请求将立即以状态代码401终止。
如果用户存在并且对象缺少匹配类型(hotp/totp)的属性mfa,则请求将被重定向到验证url(如果提供),否则将终止。此类url应提供输入代码的方式,例如:
<html>
<head>
<meta charset="UTF-8">
<title>OTP Authenticator Verification Example Page</title>
</head>
<body>
<form action="/otp/verify" method="post" enctype="multipart/form-data">
<div>
<label>Code:</label>
<input type="text" name="code"/><br/>
</div>
<div>
<input type="submit" value="Submit"/>
</div>
</form>
</body>
</html>
用户输入有效code后,请求重定到初始URL或者到/根路径(在没有原URL的情况下)
当然,这个流程假设认证器应用或者设备已经配置好。为了配置一个新的应用或设备,下面是一个HTML页面的例子:
<html>
<head>
<title>OTP Authenticator Registration Example Page</title>
</head>
<body>
<p>Scan this QR Code in Google Authenticator</p>
<img id="qrcode">
<p>- or enter this key manually -</p>
<span id="url"></span>
<script>
const key = document.getElementById('url');
const qrcode = document.getElementById('qrcode');
fetch(
'/otp/register',
{
method: 'POST',
headers: {
'Accept': 'application/json'
}
})
.then(res => {
if (res.status === 200) {
return res;
}
throw new Error(res.statusText);
})
.catch(err => console.error(err))
.then(res => res.json())
.then(json => {
key.innerText = json.url;
qrcode.src =
'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' +
encodeURIComponent(json.url);
});
</script>
</body>
</html>
本例中重要的一点是,脚本向配置的注册回调发出POST请求。再次,如果请求中没有已经认证的用户,则该回调将返回状态代码401。成功后,JSON文档将返回一个url和一些额外的元数据。该url应用于配置验证器,方法是在应用程序上手动输入或呈现QR(二维码)码。二维码的渲染可以在后端或前端完成。为了简单起见,本示例使用GoogleChartsAPI在浏览器上呈现它。
最后,这是如何在vert中使用处理程序。vert.x应用程序:
router.post()
.handler(BodyHandler.create());
// add a session handler (OTP requires state)
router.route()
.handler(SessionHandler
.create(LocalSessionStore.create(vertx))
.setCookieSameSite(CookieSameSite.STRICT));
// add the first authentication mode, for example HTTP Basic Authentication
router.route()
.handler(basicAuthHandler);
final OtpAuthHandler otp = OtpAuthHandler
.create(TotpAuth.create()
.authenticatorFetcher(authr -> {
// fetch authenticators from a database
// ...
return Future.succeededFuture(new io.vertx.ext.auth.otp.Authenticator());
})
.authenticatorUpdater(authr -> {
// update or insert authenticators from a database
// ...
return Future.succeededFuture();
}));
otp
// the issuer for the application
.issuer("Vert.x Demo")
// handle code verification responses
.verifyUrl("/verify-otp.html")
// handle registration of authenticators
.setupRegisterCallback(router.post("/otp/register"))
// handle verification of authenticators
.setupCallback(router.post("/otp/verify"));
// secure the rest of the routes
router.route()
.handler(otp);
// To view protected details, user must be authenticated and
// using 2nd factor authentication
router.get("/protected")
.handler(ctx -> ctx.end("Super secret content"));