Skip to content

Commit

Permalink
Add support for Application Only Auth (not-an-aardvark#207)
Browse files Browse the repository at this point in the history
* Add ability to make requests using Application Only Auth

* Add ability to choose either INSTALLED_CLIENT or CLIENT_CREDENTIAL for user-less token authorization

* Simplfy application auth testing credentials

* Update Babelify to 10.0.0

* Change last two .fromApplicationOnlyAuth tests to run in browser

* Document DO_NOT_TRACK_THIS_DEVICE param in fromApplicationOnlyAuth

* Add documentation tweaks regarding app-only auth
  • Loading branch information
alexerhardt authored and Eric committed Aug 19, 2019
1 parent 2f6f9ee commit ffc4ac9
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 2 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@types/request": "^2.48.2",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"babelify": "^7.3.0",
"babelify": "^10.0.0",
"browserify": "^13.1.0",
"chai": "^3.5.0",
"dirty-chai": "^1.2.2",
Expand Down
1 change: 1 addition & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Put these credentials in a file called `oauth_info.json` in the project root dir
"username": "put a username here",
"password": "put a password here",
"redirect_uri": "put the redirect URI here"
"installed_app_client_id" "put_your_installed_app_client_id_here"
}
```

Expand Down
88 changes: 88 additions & 0 deletions src/snoowrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,94 @@ const snoowrap = class snoowrap {
});
}

/**
* @summary Returns the grant types available for app-only authentication
* @desc Per the Reddit API OAuth docs, there are two different grant types depending on whether the app is an installed client
* or a confidential client such as a web app or string. This getter returns the possible values for the "grant_type" field
* in application-only auth.
* @returns {object} The enumeration of possible grant_type values
*/
static get grantType () {
return {
CLIENT_CREDENTIALS: 'client_credentials',
INSTALLED_CLIENT: 'https://oauth.reddit.com/grants/installed_client'
};
}
/**
* @summary Creates a snoowrap requester from a "user-less" Authorization token
* @desc In some cases, 3rd party app clients may wish to make API requests without a user context. App clients can request
* a "user-less" Authorization token via either the standard client_credentials grant, or the reddit specific
* extension to this grant, https://oauth.reddit.com/grants/installed_client. Which grant type an app uses depends on
* the app-type and its use case.
* @param {object} options
* @param {string} options.userAgent A unique description of what your app does. This argument is not necessary when snoowrap
is running in a browser.
* @param {string} options.clientId The client ID of your app (assigned by reddit). If your code is running clientside in a
* browser, using an "Installed" app type is recommended. From the reddit docs: "reddit *may* choose to use this ID to generate
* aggregate data about user counts. Clients that wish to remain anonymous should use the value DO_NOT_TRACK_THIS_DEVICE."
* @param {string} [options.clientSecret] The client secret of your app. Only required for "client_credentials" grant type.
* @param {string} [options.deviceId] A unique, per-device ID generated by your client. Only required
* for "Installed" grant type.
* @param {string} [options.grantType=snoowrap.grantType.INSTALLED_CLIENT] The type of "user-less"
* token to use {@link snoowrap.grantType}
* @param {string} [options.endpointDomain='reddit.com'] The endpoint domain that the returned requester should be configured
to use. If the user is authenticating on reddit.com (as opposed to some other site with a reddit-like API), you can omit this
value.
* @returns {Promise} A Promise that fulfills with a `snoowrap` instance
* @example
*
* snoowrap.fromApplicationOnlyAuth({
* userAgent: 'My app',
* clientId: 'foobarbazquuux',
* deviceId: 'unique device id'
* grantType: snoowrap.grantType.INSTALLED_CLIENT
* }).then(r => {
* // Now we have a requester that can access reddit through a "user-less" Auth token
* return r.getHot().then(posts => {
* // do something with posts from the front page
* });
* })
*
* snoowrap.fromApplicationOnlyAuth({
* userAgent: 'My app',
* clientId: 'foobarbazquuux',
* clientSecret: 'your web app secret'
* grantType: snoowrap.grantType.CLIENT_CREDENTIALS
* }).then(r => {
* // Now we have a requester that can access reddit through a "user-less" Auth token
* return r.getHot().then(posts => {
* // do something with posts from the front page
* });
* })
*/
static fromApplicationOnlyAuth ({
userAgent = isBrowser ? global.navigator.userAgent : requiredArg('userAgent'),
clientId = requiredArg('clientId'),
clientSecret,
deviceId,
grantType = requiredArg('grantType'),
endpointDomain = 'reddit.com'
}) {
return this.prototype.credentialedClientRequest.call({
clientId,
clientSecret,
// Use `this.prototype.rawRequest` function to allow for custom `rawRequest` method usage in subclasses.
rawRequest: this.prototype.rawRequest
}, {
method: 'post',
baseUrl: `https://www.${endpointDomain}/`,
uri: 'api/v1/access_token',
form: {grant_type: grantType, device_id: deviceId}
}).then(response => {
if (response.error) {
throw new Error(`API Error: ${response.error}`);
}
// Use `new this` instead of `new snoowrap` to ensure that subclass instances can be returned
const requester = new this({userAgent, clientId, clientSecret, ...response});
requester.config({endpointDomain});
return requester;
});
}
_newObject (objectType, content, _hasFetched = false) {
return Array.isArray(content) ? content : new snoowrap.objects[objectType](content, this, _hasFetched);
}
Expand Down
42 changes: 41 additions & 1 deletion test/snoowrap.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ describe('snoowrap', function () {
refresh_token: process.env.SNOOWRAP_REFRESH_TOKEN,
username: process.env.SNOOWRAP_USERNAME,
password: process.env.SNOOWRAP_PASSWORD,
redirect_uri: process.env.SNOOWRAP_REDIRECT_URI
redirect_uri: process.env.SNOOWRAP_REDIRECT_URI,
installed_app_client_id: process.env.SNOOWRAP_INSTALLED_APP_CLIENT_ID
} : require('../oauth_info.json');

r = new snoowrap({
Expand Down Expand Up @@ -219,6 +220,45 @@ describe('snoowrap', function () {
});
});

describe('.fromApplicationOnlyAuth', () => {
it('throws a TypeError if no userAgent is provided in node', function () {
if (isBrowser) {
return this.skip();
}
expect(() => {
snoowrap.fromApplicationOnlyAuth({clientId: 'bar', deviceId: 'baz'});
}).to.throw(TypeError);
});
it('throws a TypeError if no clientId is provided', () => {
expect(() => {
snoowrap.fromApplicationOnlyAuth({userAgent: 'bar'});
}).to.throw(TypeError);
});
it('throws a TypeError if no grantType is provided', () => {
expect(() => {
snoowrap.fromApplicationOnlyAuth({userAgent: 'bar'});
}).to.throw(TypeError);
});
it('returns a snoowrap instance for a valid installed requester', async () => {
const newRequester = await snoowrap.fromApplicationOnlyAuth({
userAgent: oauthInfo.user_agent,
grantType: snoowrap.grantType.INSTALLED_CLIENT,
clientId: oauthInfo.installed_app_client_id,
deviceId: oauthInfo.device_id
});
expect(await newRequester.getHot('redditdev', {limit: 1})).to.have.lengthOf(1);
});
it('returns a snoowrap instance for a valid client requester', async () => {
const newRequester = await snoowrap.fromApplicationOnlyAuth({
userAgent: oauthInfo.user_agent,
grantType: snoowrap.grantType.CLIENT_CREDENTIALS,
clientId: oauthInfo.client_id,
clientSecret: oauthInfo.client_secret
});
expect(await newRequester.getHot('redditdev', {limit: 1})).to.have.lengthOf(1);
});
});

describe('internal helpers', () => {
it('can deeply clone a RedditContent instance', async () => {
const some_user = r.getUser('someone');
Expand Down

0 comments on commit ffc4ac9

Please sign in to comment.