├── .eslintignore ├── .eslintrc ├── .gitignore ├── Makefile ├── README.md ├── lib └── client.js ├── package.json └── test └── client.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.debug.js 2 | *.min.js 3 | node_modules/* 4 | assets/scripts/lib/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [2, "always"], 16 | "strict": [2, "global"], 17 | "curly": 2, 18 | "eqeqeq": 2, 19 | "no-eval": 2, 20 | "guard-for-in": 2, 21 | "no-caller": 2, 22 | "no-else-return": 2, 23 | "no-eq-null": 2, 24 | "no-extend-native": 2, 25 | "no-extra-bind": 2, 26 | "no-floating-decimal": 2, 27 | "no-implied-eval": 2, 28 | "no-labels": 2, 29 | "no-with": 2, 30 | "no-loop-func": 1, 31 | "no-native-reassign": 2, 32 | "no-redeclare": [2, {"builtinGlobals": true}], 33 | "no-delete-var": 2, 34 | "no-shadow-restricted-names": 2, 35 | "no-undef-init": 2, 36 | "no-use-before-define": 2, 37 | "no-unused-vars": [2, {"args": "none"}], 38 | "no-undef": 2, 39 | "callback-return": [2, ["callback", "cb", "next"]], 40 | "global-require": 0, 41 | "no-console": 0, 42 | "require-yield": 0 43 | }, 44 | "env": { 45 | "es6": true, 46 | "node": true, 47 | "browser": true 48 | }, 49 | "globals": { 50 | "describe": true, 51 | "it": true, 52 | "before": true, 53 | "after": true 54 | }, 55 | "parserOptions": { 56 | "ecmaVersion": 8, 57 | "sourceType": "script", 58 | "ecmaFeatures": { 59 | "jsx": true 60 | } 61 | }, 62 | "extends": "eslint:recommended" 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | .nyc_output/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | REPORTER = spec 3 | TIMEOUT = 20000 4 | MOCHA = ./node_modules/mocha/bin/_mocha 5 | PATH := ./node_modules/.bin:$(PATH) 6 | 7 | lint: 8 | @eslint --fix lib index.js test 9 | 10 | test: 11 | @mocha -t $(TIMEOUT) -b -R spec $(TESTS) 12 | 13 | test-cov: 14 | @nyc --reporter=html --reporter=text mocha -t $(TIMEOUT) -R spec $(TESTS) 15 | 16 | test-coveralls: lint 17 | @nyc mocha -t $(TIMEOUT) -R spec $(TESTS) 18 | @echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID) 19 | @nyc report --reporter=text-lcov | coveralls 20 | 21 | .PHONY: test 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aliyun OAuth 2.0 Node.js Client 2 | 3 | This SDK is used support login in Web Application, more details see https://help.aliyun.com/document_detail/69962.html . 4 | 5 | ## Installation 6 | 7 | ```sh 8 | $ npm install @alicloud/oauth2 9 | ``` 10 | 11 | ## Usage 12 | 13 | 1. Build the client with `client id` and `client secret` 14 | 15 | ```js 16 | // Require it 17 | const AliyunOAuth2 = require('@alicloud/oauth2'); 18 | 19 | const client = new AliyunOAuth2({ 20 | clientId, 21 | clientSecret 22 | }); 23 | ``` 24 | 25 | The client can be used for All user to login with OAuth 2.0. 26 | 27 | 2. Get the authorize URL and let user redirect to the url 28 | 29 | ```js 30 | const callback = 'https://yourwebapp.com/authcallback/'; 31 | const scope = 'openid /acs/ccc'; 32 | const state = '1234567890'; 33 | const url = auth.getAuthorizeURL(callback, state, scope, 'offline'); 34 | // like 35 | // https://signin.aliyun.com/oauth2/v1/auth?client_id=123456&redirect_uri=https%3A%2F%2Fyourwebapp.com%2Fauthcallback%2F&response_type=code&scope=openid%20%2Facs%2Fccc&access_type=offline&state=1234567890 36 | ``` 37 | 38 | After user login with Aliyun Web UI, it will callback to your web app with code, like: 39 | 40 | `https://yourwebapp.com/authcallback/?code=xxx` 41 | 42 | 3. Use code to get access token 43 | 44 | ```js 45 | async function () { 46 | const reuslt = await client.getAccessToken('code'); 47 | }); 48 | ``` 49 | 50 | If ok, the result like: 51 | 52 | ```json 53 | { 54 | "access_token": "eyJraWQiOiJrMTIzNCIsImVuY...", 55 | "token_type": "Bearer", 56 | "expires_in": 3600, 57 | "refresh_token": "Ccx63VVeTn2dxV7ovXXfLtAqLLERAH1Bc", 58 | "id_token": "eyJhbGciOiJIUzI1N..." 59 | } 60 | ``` 61 | 62 | If fails, throw an error like: 63 | 64 | ```js 65 | var err = new Error(`${data.error}: ${data.error_description}`); 66 | err.name = 'OAuthServerError'; 67 | err.code = data.error; 68 | err.data = { 69 | httpcode: data.http_code, 70 | requestid: data.request_id 71 | }; 72 | throw err; 73 | ``` 74 | 75 | 4. When token expired, we can refresh it with `refresh token` 76 | 77 | ```js 78 | async function () { 79 | const reuslt = await client.refreshAccessToken('refresh token'); 80 | }); 81 | ``` 82 | 83 | The result is like getAccessToken return value. 84 | 85 | 5. Also, If need, we can revoke the token 86 | 87 | ```js 88 | async function () { 89 | const reuslt = await client.revokeAccessToken('access token'); 90 | }); 91 | ``` 92 | 93 | ## License 94 | The MIT license 95 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const querystring = require('querystring'); 4 | 5 | const httpx = require('httpx'); 6 | 7 | class Client { 8 | constructor(config) { 9 | this.clientId = config.clientId; 10 | this.clientSecret = config.clientSecret; 11 | this.accessTokenUri = config.accessTokenUri || 'https://oauth.aliyun.com/v1/token'; 12 | this.authorizationUri = config.authorizationUri || 'https://signin.aliyun.com/oauth2/v1/auth', 13 | this.revokeTokenUri = config.revokeTokenUri || 'https://oauth.aliyun.com/v1/revoke'; 14 | this.userInfoUri = config.userInfoUri || 'https://oauth.aliyun.com/v1/userinfo'; 15 | } 16 | 17 | getAuthorizeURL(redirectUri, state, scope, accessType = 'online') { 18 | const queries = { 19 | client_id: this.clientId, 20 | redirect_uri: redirectUri, 21 | response_type: 'code', 22 | scope: scope, 23 | access_type: accessType, 24 | state: state 25 | }; 26 | 27 | return `${this.authorizationUri}?${querystring.stringify(queries)}`; 28 | } 29 | 30 | async _request(url, options) { 31 | const response = await httpx.request(url, options); 32 | const body = await httpx.read(response, 'utf8'); 33 | const contentType = response.headers['content-type'] || ''; 34 | if (!contentType.startsWith('application/json')) { 35 | throw new Error(`content type invalid: ${contentType}, should be 'application/json'`); 36 | } 37 | 38 | const data = JSON.parse(body); 39 | 40 | if (data.error) { 41 | var err = new Error(`${data.error}: ${data.error_description}`); 42 | err.name = 'OAuthServerError'; 43 | err.code = data.error; 44 | err.data = { 45 | httpcode: data.http_code, 46 | requestid: data.request_id 47 | }; 48 | throw err; 49 | } 50 | 51 | return data; 52 | } 53 | 54 | /** 55 | * 根据授权获取到的code,换取access token和openid 56 | * 获取openid之后,可以调用`wechat.API`来获取更多信息 57 | * Examples: 58 | * ``` 59 | * await api.getAccessToken(code); 60 | * ``` 61 | * Exception: 62 | * 63 | * - `err`, 获取access token出现异常时的异常对象 64 | * - `message` 65 | * - `code` 66 | * - `data` 67 | * - `requestid` 68 | * 69 | * 返回值: 70 | * ``` 71 | * { 72 | * "access_token": "eyJraWQiOiJrMTIzNCIsImVuY...", 73 | * "token_type": "Bearer", 74 | * "expires_in": 3600, 75 | * "refresh_token": "Ccx63VVeTn2dxV7ovXXfLtAqLLERAH1Bc", 76 | * "id_token": "eyJhbGciOiJIUzI1N..." 77 | * } 78 | * ``` 79 | * @param {String} code 授权获取到的code 80 | */ 81 | getAccessToken(code, redirectUri = '') { 82 | const info = { 83 | code, 84 | client_id: this.clientId, 85 | client_secret: this.clientSecret, 86 | redirect_uri: redirectUri, 87 | grant_type: 'authorization_code' 88 | }; 89 | 90 | const url = `${this.accessTokenUri}?${querystring.stringify(info)}`; 91 | return this._request(url); 92 | } 93 | 94 | refreshAccessToken(refreshToken) { 95 | const info = { 96 | refresh_token: refreshToken, 97 | client_id: this.clientId, 98 | client_secret: this.clientSecret, 99 | grant_type: 'refresh_token', 100 | }; 101 | 102 | const url = `${this.accessTokenUri}?${querystring.stringify(info)}`; 103 | 104 | return this._request(url); 105 | } 106 | 107 | revokeAccessToken(accessToken) { 108 | const info = { 109 | token: accessToken, 110 | client_id: this.clientId, 111 | client_secret: this.clientSecret 112 | }; 113 | 114 | const url = `${this.revokeTokenUri}?${querystring.stringify(info)}`; 115 | return this._request(url); 116 | } 117 | 118 | getUserInfo(accessToken) { 119 | const headers = { 120 | 'Authorization': `Bearer ${accessToken}` 121 | }; 122 | 123 | const url = `${this.userInfoUri}`; 124 | return this._request(url, { 125 | headers 126 | }); 127 | } 128 | 129 | } 130 | 131 | module.exports = Client; 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alicloud/oauth2", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "httpx": "^2.1.3" 6 | }, 7 | "devDependencies": { 8 | "coveralls": "^3.0.2", 9 | "eslint": "^5.1.0", 10 | "expect.js": "^0.3.1", 11 | "mocha": "^5.2.0", 12 | "muk": "^0.5.3", 13 | "nyc": "^12.0.2" 14 | }, 15 | "main": "lib/client.js", 16 | "directories": { 17 | "lib": "lib", 18 | "test": "test" 19 | }, 20 | "scripts": { 21 | "lint": "eslint --fix lib/* test/*", 22 | "test": "mocha -R spec test/*.test.js", 23 | "test-cov": "nyc --reporter=html --reporter=text mocha -R spec test/*.test.js" 24 | }, 25 | "files": [ 26 | "lib" 27 | ], 28 | "author": "Jackson Tian", 29 | "license": "MIT", 30 | "description": "AliCloud Enterprise Application OAuth2.0 client" 31 | } 32 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('expect.js'); 4 | const muk = require('muk'); 5 | const httpx = require('httpx'); 6 | 7 | const OAuth = require('../lib/client'); 8 | 9 | const clientId = process.env.CLIENT_ID; 10 | const clientSecret = process.env.CLIENT_SECRET; 11 | 12 | describe('oauth.js', function () { 13 | 14 | describe('getAuthorizeURL', function () { 15 | var auth = new OAuth({ 16 | clientId: '123456', 17 | }); 18 | 19 | it('should ok', function () { 20 | const callback = 'https://yourwebapp.com/authcallback/'; 21 | const scope = 'openid /acs/ccc'; 22 | const state = '1234567890'; 23 | var url = auth.getAuthorizeURL(callback, state, scope, 'offline'); 24 | expect(url).to.be.equal('https://signin.aliyun.com/oauth2/v1/auth?client_id=123456&redirect_uri=https%3A%2F%2Fyourwebapp.com%2Fauthcallback%2F&response_type=code&scope=openid%20%2Facs%2Fccc&access_type=offline&state=1234567890'); 25 | }); 26 | }); 27 | 28 | describe('getAccessToken', function () { 29 | var api = new OAuth({ 30 | clientId, 31 | clientSecret 32 | }); 33 | 34 | it('should invalid', async function () { 35 | try { 36 | await api.getAccessToken('code'); 37 | } catch (err) { 38 | expect(err).to.be.ok(); 39 | expect(err.name).to.be.equal('OAuthServerError'); 40 | expect(err.message).to.be.equal('invalid_grant: code is invalid'); 41 | return; 42 | } 43 | // should never be executed 44 | expect(false).to.be.ok(); 45 | }); 46 | 47 | describe('should ok', function () { 48 | before(function () { 49 | muk(httpx, 'request', async function (url, opts) { 50 | return { 51 | headers: { 52 | 'content-type': 'application/json' 53 | } 54 | }; 55 | }); 56 | 57 | muk(httpx, 'read', async function (response, encoding) { 58 | return JSON.stringify({ 59 | 'access_token': 'eyJraWQiOiJrMTIzNCIsImVuY...', 60 | 'token_type': 'Bearer', 61 | 'expires_in': 3600, 62 | 'refresh_token': 'Ccx63VVeTn2dxV7ovXXfLtAqLLERAH1Bc', 63 | 'id_token': 'eyJhbGciOiJIUzI1N...' 64 | }); 65 | }); 66 | }); 67 | 68 | after(function () { 69 | muk.restore(); 70 | }); 71 | 72 | it('should ok', async function () { 73 | var result = await api.getAccessToken('code'); 74 | expect(result).to.have.keys('access_token', 'token_type', 'expires_in', 'refresh_token', 'id_token'); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('refreshAccessToken', function () { 80 | var api = new OAuth({ 81 | clientId, 82 | clientSecret 83 | }); 84 | 85 | it('should invalid', async function () { 86 | try { 87 | await api.refreshAccessToken('refresh_token'); 88 | } catch (err) { 89 | expect(err).to.be.ok(); 90 | expect(err.name).to.be.equal('OAuthServerError'); 91 | expect(err.message).to.be.equal('invalid_grant: invalid refreshToken'); 92 | return; 93 | } 94 | 95 | // should never be executed 96 | expect(false).to.be.ok(); 97 | }); 98 | 99 | describe('should ok', function () { 100 | before(function () { 101 | muk(httpx, 'request', async function (url, opts) { 102 | return { 103 | headers: { 104 | 'content-type': 'application/json' 105 | } 106 | }; 107 | }); 108 | 109 | muk(httpx, 'read', async function (response, encoding) { 110 | return JSON.stringify({ 111 | 'access_token': 'eyJraWQiOiJrMTIzNCIsImVuY...', 112 | 'token_type': 'Bearer', 113 | 'expires_in': 3600, 114 | 'refresh_token': 'Ccx63VVeTn2dxV7ovXXfLtAqLLERAH1Bc', 115 | 'id_token': 'eyJhbGciOiJIUzI1N...' 116 | }); 117 | }); 118 | }); 119 | 120 | after(function () { 121 | muk.restore(); 122 | }); 123 | 124 | it('should ok', async function () { 125 | var result = await api.refreshAccessToken('refresh_token'); 126 | expect(result).to.have.keys('access_token', 'token_type', 'expires_in', 'refresh_token', 'id_token'); 127 | }); 128 | }); 129 | }); 130 | 131 | describe('revokeAccessToken', function () { 132 | var api = new OAuth({ 133 | clientId, 134 | clientSecret 135 | }); 136 | 137 | it('should ok', async function () { 138 | const result = await api.revokeAccessToken('token'); 139 | expect(result.success).to.be.ok(); 140 | expect(result.message).to.be('success'); 141 | }); 142 | }); 143 | 144 | describe('getUserInfo', function () { 145 | var api = new OAuth({ 146 | clientId, 147 | clientSecret 148 | }); 149 | 150 | it('should ok', async function () { 151 | try { 152 | await api.getUserInfo('token'); 153 | } catch (err) { 154 | expect(err).to.be.ok(); 155 | expect(err.name).to.be.equal('OAuthServerError'); 156 | expect(err.message).to.be.equal('access_denied: parse token failed'); 157 | return; 158 | } 159 | // should never be executed 160 | expect(false).to.be.ok(); 161 | }); 162 | 163 | describe('should ok', function () { 164 | before(function () { 165 | muk(httpx, 'request', async function (url, opts) { 166 | return { 167 | headers: { 168 | 'content-type': 'application/json' 169 | } 170 | }; 171 | }); 172 | 173 | muk(httpx, 'read', async function (response, encoding) { 174 | return JSON.stringify({ 175 | 'exp': 1517539523, 176 | 'sub': '25993xxxxxxxx335187', 177 | 'name': 'alice', 178 | 'upn': 'alice@demo.onaliyun.com', 179 | 'aud': '45678xxxxxxxx901234', 180 | 'iss': 'https://oauth.aliyun.com', 181 | 'did': '', 182 | 'aid': '1937xxxxxxxxx9368', 183 | 'bid': '26842', 184 | 'iat': 1517535923 185 | }); 186 | }); 187 | }); 188 | 189 | after(function () { 190 | muk.restore(); 191 | }); 192 | 193 | it('should ok', async function () { 194 | var result = await api.getUserInfo('token'); 195 | expect(result).to.have.keys('name', 'upn', 'aud', 'iss', 'did', 'aid', 'bid', 'iat'); 196 | }); 197 | }); 198 | }); 199 | }); 200 | --------------------------------------------------------------------------------