3

我一直在尝试实现单点登录(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

认证流程

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

失败场景:

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

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}`)
})

4

3 回答 3

0

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

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

例子

  1. 使用以下重定向 URI 配置您的应用https://foo1.bar.com/connect/google/callback
  2. https://foo1.bar.com/login在您的浏览器应用中导航到
  3. 浏览器应用重定向到您的 APIhttps://api.bar.com/connect/google
  4. 在将用户重定向到 Google 之前,上面的代码根据请求redirect_uri的传入标头配置Hosthttps://foo1.bar.com/connect/google/callback
  5. 用户登录 Google 并被重定向回https://foo1.bar.com/connect/google/callback
  6. 该特定路由必须重定向回您的 APIhttps://api.bar.com/connect/google/callback

重复https://foo2.bar.com

于 2020-08-22T08:48:32.373 回答
-1

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

要了解有关中继状态的更多信息:https ://developer.okta.com/docs/concepts/saml/

您使用的是哪种 SSO 服务?

于 2020-08-18T05:44:58.323 回答
-1

我通过删除 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/oauth端点中,我没有覆盖状态,而是创建了一个哈希图stateMap并将其添加到会话中,并将其uiState作为在 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"
}
于 2020-09-01T06:43:42.000 回答