7

最近我一直在阅读 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 密钥/秘密身份验证?

4

2 回答 2

15

让我们检查您的问题

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

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

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

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

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

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

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

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

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

授权服务器必须:

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

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

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

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

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

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

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

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

我们来看例子

存储.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;

应用程序.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 标头中发送令牌,以使用获得的令牌授权每个请求。

于 2016-03-29T21:26:11.903 回答
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 连接会这样做吗?

是的。谷歌使用 openid 连接

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

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

于 2016-03-28T18:00:27.560 回答