如何解决在同一浏览器上同时访问两个应用程序模块且用户尚未登录时,授予提供者OAuth状态不匹配 身份验证流程失败情况:期望示例实现API服务器示例
我一直在尝试实现Single SignOn(SSO)。我有不同的前端应用程序模块,它们在不同的域上运行,它们都使用一个API服务器。
- SSO服务器https://sso.app.com
- API服务器https://api.app.com
- 前端模块1 https://module-1.app.com
- 前端模块2 https://module-2.app.com
身份验证流程
身份验证流程是FrontEnd Module检查本地存储中的令牌。如果找不到令牌,则会将用户重定向到API服务器端点,例如说https://api.app.com/oauth/connect。 API服务器具有SSO服务器的clientId和Secrets。 API服务器在cookie中设置Frontend模块的url(以便我可以将用户重定向回启动程序前端模块),然后将请求重定向到SSO服务器,在此向用户显示登录屏幕。用户在此处输入凭据,SSO服务器验证凭据,创建会话。 凭据经过验证后,SSO服务器将使用用户个人资料和access_token调用API服务器端点。 API服务器在会话中获取配置文件并进行查询和签名,并通过查询参数将其发送到前端模块。在frontEnd(React APP)上有一条专门为此目的的路线。在该前端路由中,我从queryParams中提取令牌,并在localstorage中进行设置。用户在应用程序中。 类似地,当用户加载FrontendModule-2时,发生了相同的流,但这一次是因为在FrontendModule-1流运行时,会话由SSO服务器创建。它永远不会要求登录凭据并让用户登录系统。
失败情况:
方案是,假设有一个尚未登录并且没有会话的用户JHON。 Jhon在浏览器中点击了“前端模块1” URL。前端模块检查本地存储中是否有令牌,但找不到该令牌,然后前端模块将用户重定向到API服务器路由。 API服务器具有clientSecret和clientId,它们将请求重定向到SSO服务器。该用户将看到登录屏幕。
Jhon看到登录屏幕,并保持原样。现在,Jhon在同一浏览器中打开另一个选项卡,然后输入“前端模块2”的URL。与上述相同,Jhon登陆登录屏幕。 Jhon照原样离开了该屏幕,然后移回到第一个选项卡,在那里他已加载了Frontend Module 1会话屏幕。他输入凭据并单击登录按钮。会话状态已更改给我错误。 该错误实际上是有道理的,因为会话是共享的。
期望
如何在没有错误的情况下实现这一目标。我想将用户重定向到发起请求的同一前端模块。
我正在使用的工具
示例实现(API服务器)
require('dotenv').config();
var express = require('express'),session = require('express-session'),morgan = require('morgan')
var Grant = require('grant-express'),port = process.env.PORT || 3001,oauthConsumer= process.env.OAUTH_CONSUMER || `http://localhost`,oauthProvider = process.env.OAUTH_PROVIDER_URL || 'http://localhost',grant = new Grant({
defaults: {
protocol: 'https',host: oauthConsumer,transport: 'session',state: true
},myOAuth: {
key: process.env.CLIENT_ID || 'test',secret: process.env.CLIENT_SECRET || 'secret',redirect_uri: `${oauthConsumer}/connect/myOAuth/callback`,authorize_url: `${oauthProvider}/oauth/authorize`,access_url: `${oauthProvider}/oauth/token`,oauth: 2,scope: ['openid','profile'],callback: '/done',scope_delimiter: ' ',dynamic: ['uiState'],custom_params: { deviceId: 'abcd',appId: 'com.pud' }
}
})
var app = express()
app.use(morgan('dev'))
// REQUIRED: (any session store - see ./examples/express-session)
app.use(session({secret: 'grant'}))
// Setting the FrontEndModule URL in the Dynamic key of Grant.
app.use((req,res,next) => {
req.locals.grant = {
dynamic: {
uiState: req.query.uiState
}
}
next();
})
// mount grant
app.use(grant)
app.get('/done',(req,res) => {
if (req.session.grant.response.error) {
res.status(500).json(req.session.grant.response.error);
} else {
res.json(req.session.grant);
}
})
app.listen(port,() => {
console.log(`READY port ${port}`)
})
解决方法
您必须将用户重定向回原始的 app URL,而不是API服务器URL:
.use('/connect/:provider',(req,res,next) => {
res.locals.grant = {dynamic: {redirect_uri:
`http://${req.headers.host}/connect/${req.params.provider}/callback`
}}
next()
})
.use(grant(require('./config.json')))
然后您需要同时指定两者:
https://foo1.bar.com/connect/google/callback
https://foo2.bar.com/connect/google/callback
作为OAuth应用允许的重定向URI。
最后,您必须路由一些应用程序域路由到您的API服务器,格兰特(Grant)正在其中处理重定向URI。
示例
- 使用以下重定向URI
https://foo1.bar.com/connect/google/callback
配置您的应用
- 在浏览器应用中导航至
https://foo1.bar.com/login
- 浏览器应用重定向到您的API
https://api.bar.com/connect/google
- 在将用户重定向到Google之前,以上代码根据向
redirect_uri
的请求传入的Host
标头配置https://foo1.bar.com/connect/google/callback
- 用户登录Google并被重定向回
https://foo1.bar.com/connect/google/callback
- 该特定路由必须重定向回到您的API
https://api.bar.com/connect/google/callback
重复https://foo2.bar.com
在点击SSO服务器时,您具有 relay_state 选项,该选项在发送到SSO服务器时返回,只是为了在请求SSO之前跟踪应用程序状态。
要了解有关中继状态的更多信息:https://developer.okta.com/docs/concepts/saml/
您正在使用哪种SSO服务?
,我通过删除Grant-express实现并使用client-oauth2软件包来解决此问题的方式。
这是我的实现方式。
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
const session = require('express-session');
const { JWT } = require('jose');
const crypto = require('crypto');
const ClientOauth2 = require('client-oauth2');
var logger = require('morgan');
var oauthRouter = express.Router();
const clientOauth = new ClientOauth2({
clientId: process.env.CLIENT_ID,clientSecret: process.env.SECRET,accessTokenUri: process.env.ACCESS_TOKEN_URI,authorizationUri: process.env.AUTHORIZATION_URI,redirectUri: process.env.REDIRECT_URI,scopes: process.env.SCOPES
});
oauthRouter.get('/oauth',async function(req,next) {
try {
if (!req.session.user) {
// Generate random state
const state = crypto.randomBytes(16).toString('hex');
// Store state into session
const stateMap = req.session.stateMap || {};
stateMap[state] = req.query.uiState;
req.session.stateMap = stateMap;
const uri = clientOauth.code.getUri({ state });
res.redirect(uri);
} else {
res.redirect(req.query.uiState);
}
} catch (error) {
console.error(error);
res.end(error.message);
}
});
oauthRouter.get('/oauth/callback',next) {
try {
// Make sure it is the callback from what we have initiated
// Get uiState from state
const state = req.query.state || '';
const stateMap = req.session.stateMap || {};
const uiState = stateMap[state];
if (!uiState) throw new Error('State is mismatch');
delete stateMap[state];
req.session.stateMap = stateMap;
const { client,data } = await clientOauth.code.getToken(req.originalUrl,{ state });
const user = JWT.decode(data.id_token);
req.session.user = user;
res.redirect(uiState);
} catch (error) {
console.error(error);
res.end(error.message);
}
});
var app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
secret: 'My Super Secret',saveUninitialized: false,resave: true,/**
* This is the most important thing to note here.
* My application has wild card domain.
* For Example: My server url is https://api.app.com
* My first Frontend module is mapped to https://module-1.app.com
* My Second Frontend module is mapped to https://module-2.app.com
* So my COOKIE_DOMAIN is app.com. which would make the cookie accessible to subdomain.
* And I can share the session.
* Setting the cookie to httpOnly would make sure that its not accessible by frontend apps and
* can only be used by server.
*/
cookie: { domain: process.env.COOKIE_DOMAIN,httpOnly: true }
}));
app.use(express.static(path.join(__dirname,'public')));
app.use('/connect',oauthRouter);
// catch 404 and forward to error handler
app.use(function(req,next) {
next(createError(404));
});
// error handler
app.use(function(err,req,next) {
// set locals,only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
在我的/connect/oauth
端点中,我创建了一个哈希表stateMap
而不是覆盖状态,并将其添加到与uiState
的会话中,作为在URL中像这样{{1 }}
在回调中时,我从OAuth服务器获取了状态,并使用stateMap获得了https://api.foo.bar.com?uiState=https://module-1.app.com
值。
样本stateMap
uiState
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。