【问题标题】:What authentication strategy to use? [closed]使用什么身份验证策略? [关闭]
【发布时间】:2023-03-23 01:39:02
【问题描述】:

最近我一直在阅读 OAuth2、OpenID Connect 等方面的内容。但仍然对何时使用以及如何实现它感到非常迷茫。我现在正在考虑使用 NodeJS。

假设我想创建一个博客服务。该服务将公开 API 供客户使用。 “客户”包括管理 CMS。我认为解耦我的服务器和客户端(UI)会很好。我可以在不接触服务器的情况下更改 UI。这些客户端很可能是单页 Web 应用程序。

好的第一个问题:在这个例子中,我应该使用 OAuth2 吗?为什么?仅仅是因为我授权管理应用程序通过博客访问吗?

自 SPA 以来,我认为正确的策略是 OAuth2 隐式流?

对于每个应用程序,例如。 admin cms,我将不得不生成一个传递给身份验证服务器的 AppID。不需要应用程序密码对吗?

在这种情况下是否可以使用谷歌登录(而不是用户名/密码)? OpenID 连接会这样做吗?

如何在 NodeJS 中实现所有这些?我看到https://github.com/jaredhanson/oauth2orize,但我看不到如何实现隐式流程。

我确实看到了一个非官方的例子https://github.com/reneweb/oauth2orize_implicit_example/blob/master/app.js,但我在想为什么需要会话?我认为令牌的目标之一是使服务器可以是无状态的?

我也想知道,什么时候应该使用 API 密钥/秘密身份验证?

【问题讨论】:

    标签: node.js authentication openid-connect oauth2


    【解决方案1】:

    让我们检查您的问题

    1. 我应该使用 OAuth2 吗?为什么?

    A:嗯,因为今天旧的 OpenId 2 身份验证协议已被标记为过时(2014 年 11 月),而 OpenId Connect 是建立在 OAuth2 之上的身份层,所以真正的问题是了解和验证用户身份(身份验证部分)是否对您和您的企业很重要。如果答案是“是”,则选择 OpenId Connect,否则您可以选择两者中的任何一个,您觉得更舒服的那个。

    1. 自 SPA 以来,我认为正确的策略是 OAuth2 隐式流?

    A: 没有。您可以在使用 SPA 时实施任何策略,有些策略比其他策略需要更多的工作,并且很大程度上取决于您要完成的工作。隐式流程是最简单的,但它不会对您的用户进行身份验证,因为访问令牌是直接颁发的。

    在隐式授权流程中发布访问令牌时,授权服务器不会对客户端进行身份验证。在某些情况下,可以通过用于将访问令牌传递给客户端的重定向 URI 来验证客户端身份。

    我不会为您的应用(或任何需要相当安全级别的应用1)推荐此流程。

    如果您想保持简单,您应该使用带有用户名和密码的Resource Owner Grant 流程,但同样没有什么可以阻止您实施Authorization Code Grant 流程,特别是如果您希望允许第三方应用程序使用您的服务(在我看来这是一个成功的策略)并且它会比其他的更安全,因为它需要用户的明确同意。

    1. 对于每个应用程序,例如。 admin cms,我将不得不生成一个传递给身份验证服务器的 AppID。不需要应用密码对吗?

    答:是的,这是正确的,但是当您无法使用基本身份验证时,可以使用 client_secret 为资源所有者流程中的令牌端点添加额外的安全层,这在任何其他流程中都不需要。@ 987654327@3

    授权服务器必须:

    • 要求对机密客户端或任何客户端进行客户端身份验证 已颁发客户端凭据的客户端(或其他 身份验证要求),

    • 如果包含客户端身份验证,则对客户端进行身份验证,并且

    • 使用其验证资源所有者密码凭据 现有的密码验证算法。

    或者,授权服务器可以支持在请求正文中包含客户端凭据 (...) 不推荐使用这两个参数在请求正文中包含客户端凭据,并且应该仅限于无法直接使用的客户端HTTP Basic 身份验证方案(或其他基于密码的 HTTP 身份验证方案)

    1. 在这种情况下是否可以使用谷歌登录(而不是用户名/密码)? OpenID 连接会这样做吗?

    答:是的,可以使用 google 登录,在这种情况下,您只是将身份验证和授权工作委托给 google 服务器。使用授权服务器的好处之一是能够通过一次登录来访问其他资源,而无需为要访问的每个资源创建本地帐户。

    1. 如何在 NodeJS 中实现所有这些?

    你从右脚开始。使用oaut2horize 是实现授权服务器颁发令牌的最简单方法。我测试的所有其他库的使用都过于复杂,并且与 node 和 express 集成(免责声明:这只是我的观点)。 OAuthorize 与 passport.js(均来自同一作者)很好地配合使用,这是一个很好的框架,可以通过 300 多种策略(如 google、facebook、github 等)执行身份验证和授权。您可以使用 passport-google(已过时)轻松集成 google )、passport-google-oauthpassport-google-plus

    我们来看例子

    storage.js

        // An array to store our clients. You should likely store this in a
        // in-memory storage mechanism like Redis
        // you should generate one of this for any of your api consumers
        var clients = [
            {id: 'as34sHWs34'}
            // can include additional info like:
            // client_secret or password
            // redirect uri from which client calls are expected to originate
        ];
        // An array to store our tokens. Like the clients this should go in a memory storage
        var tokens = [];
    
        // Authorization codes storage. Those will be exchanged for tokens at the end of the flow. 
        // Should be persisted in memory as well for fast access.
        var codes = [];
    
        module.exports = {
            clients: clients,
            tokens: tokens,
            codes: codes
        };
    

    oauth.js

        // Sample implementation of Authorization Code Grant
    
        var oauth2orize = require('oauth2orize');
        var _ = require('lodash');
        var storage = require('./storage');
    
        // Create an authorization server
        var server = oauth2orize.createServer();
    
        // multiple http request responses will be used in the authorization process 
        // so we need to store the client_id in the session 
        // to later restore it from storage using only the id
        server.serializeClient(function (client, done) {
            // return no error so the flow can continue and pass the client_id.
            return done(null, client.id);
        });
    
        // here we restore from storage the client serialized in the session 
        // to continue negotiation
        server.deserializeClient(function (id, done) {
            // return no error and pass a full client from the serialized client_id
            return done(null, _.find(clients, {id: id}));
        });
    
        // this is the logic that will handle step A of oauth 2 flow
        // this function will be invoked when the client try to access the authorization endpoint
        server.grant(oauth2orize.grant.code(function (client, redirectURI, user, ares, done) {
            // you should generate this code any way you want but following the spec
            // https://www.rfc-editor.org/rfc/rfc6749#appendix-A.11
            var generatedGrantCode = uid(16);
            // this is the data we store in memory to use in comparisons later in the flow
            var authCode = {code: generatedGrantCode, client_id: client.id, uri: redirectURI, user_id: user.id};
    
            // store the code in memory for later retrieval
            codes.push(authCode);
    
            // and invoke the callback with the code to send it to the client
            // this is where step B of the oauth2 flow takes place.
            // to deny access invoke an error with done(error);
            // to grant access invoke with done(null, code);
            done(null, generatedGrantCode);
        }));
    
        // Step C is initiated by the user-agent(eg. the browser)
    
        // This is step D and E of the oauth2 flow
        // where we exchange a code for a token
        server.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, done) {
            var authCode = _.find(codes, {code: code});
            // if the code presented is not found return an error or false to deny access
            if (!authCode) {
                return done(false);
            }
            // if the client_id from the current request is not the same that the previous to obtain the code
            // return false to deny access
            if (client.id !== authCode.client_id) {
                return done(null, false);
            }
            // if the uris from step C and E are not the same deny access
            if (redirectURI !== authCode.uri) {
                return done(null, false);
            }
    
            // generate a new token
            var generatedTokenCode = uid(256);
            var token = {token: generatedTokenCode, user_id: authCode.user_id, client_id: authCode.client_id};
    
            tokens.push(token);
            // end the flow in the server by returning a token to the client
            done(null, token);
        }));
    
        // Sample utility function to generate tokens and grant codes. 
        // Taken from oauth2orize samples
        function uid(len) {
            function getRandomInt(min, max) {
                return Math.floor(Math.random() * (max - min + 1)) + min;
            }
    
            var buf = []
                , chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
                , charlen = chars.length;
    
            for (var i = 0; i < len; ++i) {
                buf.push(chars[getRandomInt(0, charlen - 1)]);
            }
    
            return buf.join('');
        }
    
        module.exports = server;
    

    app.js

        var express = require('express');
        var passport = require('passport');
        var AuthorizationError = require('oauth2orize').AuthorizationError;
        var login = require('connect-ensure-login');
        var storage = require('./storage');
        var _ = require('lodash');
    
        app = express();
    
        var server = require('./oauthserver');
    
        // ... all the standard express configuration
        app.use(express.session({ secret: 'secret code' }));
        app.use(passport.initialize());
        app.use(passport.session());
    
        app.get('/oauth/authorize',
            login.ensureLoggedIn(),
            server.authorization(function(clientID, redirectURI, done) {
                var client = _.find(storage.clients, {id: clientID});
                if (client) {
                    return done(null, client, redirectURI);
                } else {
                    return done(new AuthorizationError('Access denied'));
                }
            }),
            function(req, res){
                 res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client });
            });
    
        app.post('/oauth/authorize/decision',
            login.ensureLoggedIn(),
            server.decision()
        );
    
        app.post('/oauth/token',
            passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
            server.token(),
            server.errorHandler()
        );
    
    1. (...) 但我在想的是为什么需要会话?我认为令牌的目标之一是使服务器可以是无状态的?

    当客户端将用户重定向到用户授权端点时,就会启动授权事务。要完成交易,用户必须验证并批准授权请求。因为这可能涉及多个 HTTP 请求/响应交换,所以事务存储在会话中。

    是的,但是会话用于令牌协商过程。稍后,您强制授权在 Authorization 标头中发送令牌,以使用获得的令牌授权每个请求。

    【讨论】:

    • 感谢您非常明确的回答。我确实有几个问题。 "` passport.authenticate(['basic', 'oauth2-client-password'])": In many examples a string is passed in this case its an array. Why? Is it wrong to just use 'oauth2-client-password'? Also if I want to allow login with Google, I will replace this with passport-google-oauth`?但是链接id令牌的代码在哪里?给应用用户?是因为这不是 OAuth 的要求吗?
    • 还有关于隐式授权/流的安全性。它的安全性较低,因为令牌存储在客户端?令牌没有通过其他方法传递给客户端吗?其安全性较低的原因是不需要验证客户端密码?但真的是这样吗?黑客仍然可以充当客户端调用服务器请求令牌?
    • passport.authenticate方法返回一个中间件函数,语法为passport.authenticate('strategy')passport.authenticate(['strategy1', 'strategy2'])。他们按照定义的相同顺序对您的请求进行身份验证,第一个是basic,第二个是client credentials。有关一些策略的示例,请参阅github.com/jaredhanson/oauth2orize/blob/master/examples/…,并查看他们的 github 存储库
    • 这些应用到您的应用程序中定义的现有路由(例如:app.get、'app.post' 或 Router.post),因为 express 按定义的顺序执行中间件调用。身份验证和授权通常位于此管道的开头。这意味着对 /oauth/token 的调用将执行 Basic,然后是 Client Credentials 身份验证,然后它将发出一个令牌。您的其他路线应该受到类似的策略passport.authenticate('oauth2')passport.authenticate('bearer') 的保护。查看passportjs.org/docs中的策略
    • 哦,所以 basic 将验证用户和 oauth2-client-password 应用程序。我也觉得我很困惑,因为我认为授权服务器和应用程序 Web 服务器是同一件事(就像在我的情况下在同一个 NodeJS 应用程序中一样,因为它是一个小应用程序)
    【解决方案2】:

    根据我的经验,OAuth2 是保护 API 的标准方法。我建议使用 OpenID Connect,因为它将身份验证添加到 OAuth2 的其他基于授权的规范中。您还可以在“客户”之间获得单点登录。

    自从有了 SPA,我认为正确的策略是 OAuth2 隐式流?

    将您的客户端和服务器解耦是一个不错的概念(我通常也会这样做)但是,我建议使用授权代码流,因为它不会将令牌暴露给浏览器。阅读http://alexbilbie.com/2014/11/oauth-and-javascript/。改为使用瘦服务器端代理将令牌添加到请求中。不过,我通常会避免在客户端上使用任何服务器生成的代码(例如 java 中的 JSP 或 rails 中的 erb/haml),因为它过多地将客户端与服务器耦合。

    对于每个应用程序,例如。 admin cms,我将不得不生成一个传递给身份验证服务器的 AppID。不需要应用密码对吗?

    对于隐式流,您需要一个客户端 ID。如果您使用授权代码流(推荐),您将需要 ID 和密码,但密码将保存在瘦服务器端代理中,而不是仅客户端应用程序中(因为它不能保密案例)

    在这种情况下是否可以使用谷歌登录(而不是用户名/密码)? OpenID 连接会这样做吗?

    是的。 Google 使用 openid 连接

    如何在 NodeJS 中实现所有这些?我看到https://github.com/jaredhanson/oauth2orize,但是我没有看到如何实现隐式流。

    openid connect 的一个好处是(如果您使用像 google 这样的其他提供程序),您不必自己实现该提供程序,您只需要编写客户端代码(和/或使用客户端库)。请参阅http://openid.net/developers/libraries/ 了解不同的认证实施。有关 nodejs,请参阅 https://www.npmjs.com/package/passport-openidconnect

    【讨论】:

    • 根据此处的文档:tools.ietf.org/html/rfc6749#page-24 授权代码授予“针对机密客户进行了优化”。最好将单页应用程序视为不是机密客户端,因为它的大部分(如果不是全部)逻辑都是在浏览器中运行的 javascript 中实现的。因此,我会说,Jiew Meng 最初提出的使用隐式流程的建议是正确的。
    • @Przemek Implicit 对规范进行了简化,因为纯 SPA 和本地移动客户端无法保密(如您所说)。如果隐式是一个人想要的,当然,简化......但要知道它是以令牌暴露给浏览器和无法使用刷新令牌进行离线访问为代价的。我以前实现过隐式流程,现在更喜欢授权代码流程的安全性和功能优势(当我有选择时)。我提倡一种思想服务器端代理,在浏览器中使用 SPA 时将令牌和客户端保密服务器端。
    • 我想知道授权码流更安全吗?令牌永远不会传递给客户端吗?那么它使用服务器端会话吗?黑客也不能像使用隐式流程那样充当客户端来向服务器进行身份验证吗?还是我误会了?
    • 是的。它更安全,因为令牌从不通过浏览器。 AS 向浏览器发回一个对任何人都无用的“代码”,除非他们也拥有客户端密码(仅存在于服务器代理上)。服务器代理使用代码(来自浏览器)和客户端密钥来请求令牌(所有服务器端)。使用授权代码流,您的“客户端”包括 SPA + 瘦服务器端代理,因此令牌被传递给客户端——但仅在服务器代理上。 openid.net/specs/openid-connect-basic-1_0.html#CodeFlow
    • @sdoxsee,所以服务器代理将使用会话,而 SPA 从不使用任何令牌?
    猜你喜欢
    • 2017-05-20
    • 2020-07-03
    • 2019-06-12
    • 2019-03-23
    • 1970-01-01
    • 2013-07-24
    • 2018-12-01
    • 2019-12-14
    • 2019-04-30
    相关资源
    最近更新 更多