当前位置: 首页 > 知识库问答 >
问题:

在同一浏览器上同时访问两个应用程序模块且用户尚未登录时,授予提供程序OAuth状态不匹配

宇文兴言
2023-03-14

我一直在尝试实现单点登录(SSO)。我有不同的前端应用程序模块,它们运行在不同的域上,它们都使用单个API服务器。

  1. SSO服务器https://sso.app.com
  2. API服务器https://api.app.com
  3. 前端模块1https://module-1.app.com
  4. 前端模块2https://module-2.app.com

身份验证的流程是前端模块检查本地存储中的令牌。如果找不到令牌,它会将用户重定向到API服务器endpoint,比如说https://api.app.com/oauth/connect.API服务器具有SSO服务器的客户端ID和机密。API服务器在cookie中设置前端模块的url(以便我可以将用户重定向回启动器前端模块),然后将请求重定向到SSO服务器,在该服务器上,用户将显示登录屏幕。用户在那里输入凭据,SSO服务器验证凭据,创建会话。验证凭证后,SSO服务器使用用户配置文件和访问令牌调用API服务器endpoint。API服务器在会话和查询中获取配置文件,并对自己的令牌进行签名,然后通过查询参数将其发送到前端模块。在前端(React应用程序)上有一条专门用于此目的的路线。在该前端路由中,我从queryParams中提取令牌并在localstorage中设置。用户在应用程序中。同样,当用户加载FrontendModule-2时,也会发生相同的流,但这一次是因为在FrontendModule-1流运行时,SSO服务器正在创建会话。它从不要求用户提供登录凭据并登录系统。

该场景是,假设有一个用户JHON尚未登录并且没有会话。Jhon在浏览器中点击“前端模块1”URL。前端模块检查本地存储中的令牌,但在那里找不到,然后前端模块将用户重定向到API服务器路由。API服务器具有将请求重定向到SSO服务器的clientSecret和clientId。将向用户显示登录屏幕。

Jhon看到登录屏幕并保持原样。现在,Jhon在同一浏览器中打开另一个选项卡,并输入“前端模块2”的URL。同样的流程如上所述,Jhon登陆登录屏幕。Jhon离开了原来的屏幕,移回第一个选项卡,在那里加载了前端模块1会话屏幕。他输入creds并点击登录按钮。会话状态已更改,这给了我一个错误。这个错误实际上是有道理的,因为会话是共享的。

如何在没有错误的情况下实现这一点。我想将用户重定向到发起请求的同一前端模块。

我正在使用的工具

  • NodeJS
js lang-js prettyprint-override">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}`)
})

共有3个答案

耿敏达
2023-03-14

我通过删除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, res, 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', async function(req, res, 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, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, 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/oauthendpoint中,我没有覆盖状态,而是创建一个hashmapstateMap,并将其作为url中接收到的值添加到会话中,如下https://api.foo.bar.com?uiState=https://module-1.app。com在回调中,我从OAuth服务器获取状态,并使用stateMap获取uiState值。

req.session.stateMap = {
  "12313213dasdasd13123123": "https://module-1.app.com",
  "qweqweqe131313123123123": "https://module-2.app.com"
}
孔经武
2023-03-14

在点击SSO服务器时,您有中继状态选项,该选项在发送到SSO服务器时返回,只是为了在请求SSO之前跟踪应用程序状态。

要了解有关中继状态的更多信息,请执行以下操作:https://developer.okta.com/docs/concepts/saml/

您使用的是哪种SSO服务??

上官凯泽
2023-03-14

您必须将用户重定向回原始应用程序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。

最后,您必须将一些应用程序域路由路由路由到Grant正在处理重定向URI的API服务器。

https://foo2.bar.com

 类似资料:
  • 因此,使用授权授予类型的基本OAuth2流通常如下所示(例如,假设OAuth客户端=Quora,OAuth服务器=Google): 用户转到客户端,被重定向到服务器登录页面进行身份验证。 用户登录到服务器,服务器返回authorization_code到客户端。 客户端然后调用client_id、client_secret和authorization_code服务器来获取令牌。 服务器验证并回复令

  • 我有两个spring boot应用程序。 模块1在端口8080上运行 模块2在端口9090上运行 我已在应用程序中使用此属性设置端口。属性文件 这两个模块都有/登录/注册,无需通过下面的代码进行身份验证即可访问。 任何其他请求都需要对用户进行身份验证。 如果我一次使用一个模块没有问题, 但是如果尝试同时来回使用它们,那么问题是我每次使用另一个应用程序时都必须再次登录到之前的应用程序。例如。 转到模

  • 我的web应用程序希望为每个用户创建一个DocuSign帐户。每个用户都可以发送文档以代表自己签名。我正在使用授权代码授权流。 我已经在我的应用程序中为两个用户创建了两个文档签名帐户。用户A登录并即将发送文档。他被重定向到此URI:https://account-d.docusign.com/oauth/auth?response_type=code 然后用户A从我的应用程序中注销,用户B登录。当

  • 我正在使用代码优先和EF6创建一个小项目,但现在我面临一个问题: 如果一个实体有一个自动ID PK,并且例如3个实体(ID:1,ID:2,ID:4-Id3已删除),那么克隆上下文将具有(ID:1,ID:2,ID:3)的实体。 我的问题是:有没有办法强制AI PK的值,或者至少强制跳过ID?或者,是否有通过dbcontexts将数据库复制到另一个数据库的最佳方法? 我在这里发现了一篇关于编程生成ID

  • 当Web应用程序在Docker中被容器化时,我无法让我的ASP. NET Web应用程序提供给我的浏览器。 我正在运行一个Mac,我已经使用Visual Studio Code创建了一个ASP. NET Web应用程序。这是一个简单的开箱即用的演示,它基于“空应用程序”当运行本机(在Docker之外)时,此应用程序提供Hello World!http://localhost:5000很好。换句话说

  • 我必须使用selenium脚本登录gmail帐户,但我收到了错误消息 正如从其他来源读到的,谷歌不允许使用自动化脚本登录 机器:MacOS编程语言:Java 有人知道怎么解决吗?