在同一浏览器上同时访问两个应用程序模块且用户尚未登录时,授予提供者OAuth状态不匹配 身份验证流程失败情况:期望示例实现API服务器示例

如何解决在同一浏览器上同时访问两个应用程序模块且用户尚未登录时,授予提供者OAuth状态不匹配 身份验证流程失败情况:期望示例实现API服务器示例

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

  1. SSO服务器https://sso.app.com
  2. API服务器https://api.app.com
  3. 前端模块1 https://module-1.app.com
  4. 前端模块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。

示例

  1. 使用以下重定向URI https://foo1.bar.com/connect/google/callback
  2. 配置您的应用
  3. 在浏览器应用中导航至https://foo1.bar.com/login
  4. 浏览器应用重定向到您的API https://api.bar.com/connect/google
  5. 在将用户重定向到Google之前,以上代码根据向redirect_uri的请求传入的Host标头配置https://foo1.bar.com/connect/google/callback
  6. 用户登录Google并被重定向回https://foo1.bar.com/connect/google/callback
  7. 该特定路由必须重定向回到您的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 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 <select id="xxx"> SELECT di.id, di.name, di.work_type, di.updated... <where> <if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 <property name="dynamic.classpath" value="tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -> systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping("/hires") public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate<String
使用vite构建项目报错 C:\Users\ychen\work>npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-