This guide is aimed at community developers who want to integrate with the Nexus Mods API using OAuth 2.0 and Proof Key Code Exchange (PKCE). Examples are shown in JavaScript/Node.js, but the process applies to any language.
OAuth 2.0 is an industry-standard authorization framework used to let applications access resources (such as mod data) on behalf of a user, without them having to share their password with the app.
Proof Key Code Exchange (PKCE) is an extension of OAuth 2.0 to increase the security of public-facing apps by ensuring that the user's authorization code is only readible by the same application that requested it, even if it gets intercepted mid-flight.
All applications will require OAuth 2.0 to interact with the Nexus Mods API on behalf of a user. Apps using the API will fall into one of two categories:
client secret
which is used for authorization.Applications using the Nexus Mods API must comply with the API Acceptable Use Policy. Failure to meet these standards, intentionally or otherwise, may result in Nexus Mods taking action to prevent further use of our data by your application.
In order to get started, applications must have a Client ID which is obtained by registering your app with Nexus Mods.
Currently, there is no web-based UI for this, so to register an OAuth client, the developer must email [email protected] with the following details:
This process is likely to be well documented in the OAuth modules available in your preferred coding language, so we will simply go over the sequence of operations here. Many libraries can figure out the requirements for themselves if they are provided with the "well-known" data available at https://users.nexusmods.com/.well-known/openid-configuration
If you're developing a public app, you'll need to start by generating a verifier
and challenge
for PKCE.
The verifier
is simply a cryptographically random string.
const crypto = require("crypto");
// code verifier must be a random string with a minimum of 43 characters.
// https://tools.ietf.org/html/rfc7636#section-4.1
const codeVerifier = crypto.randomBytes(43).toString("hex");
The challenge
is a sha256 hash of the verifier that has been base64 encoded.
/* BASE64 URL FUNCTION MAKES THE RESULTING KEY URL SAFE */
const base64url = str => {
return str.replace( /\+/g, "-" ).replace( /\//g, "_" ).replace( /=+$/, "" );
};
/* Code Challenge is generated by created a SHA256 hash of the verifier */
const codeChallenge = base64url(
crypto.createHash( "sha256" )
.update( codeVerifier )
.digest( "base64" )
);
All applications will then generate an Authorization URL which will need to be opened in a browser for the user to accept the app having access to their account.
// Redirect URI - NOTE: This could also use a custom protocol E.g. nxm://oauth/callback
const redirectUri = `http://localhost:8089/callback`;
// OAuth Application ID
const clientId = 'public_test';
const buildAuthorizeUrl = ( codeChallenge ) => {
const params = new URLSearchParams({
client_id: clientId,
response_type: 'code',
scope: '',
redirect_uri: redirectUri,
state: uuid(),
// The values below can be omitted for private apps
code_challenge_method: "S256",
code_challenge: codeChallenge
});
return 'https://users.nexusmods.com/oauth/authorize?' + params.toString();
};
The resulting link will look something like this:
https://users.nexusmods.com/oauth/authorize?client_id=public_test&response_type=code&scope=&redirect_uri=http%3A%2F%2Flocalhost%3A8089%2Fcallback&state=uuid&code_challenge_method=S256&code_challenge=086d23129028fba8a4850d77ed6bb0c3ef3a61b05dd03450edba8b24544a6275
Once the user allows to access their account, a code is sent back to the requesting app which can be exchanged for a token.
The Callback URI will be appended with the code
parameter. This pattern can be picked up by most OAuth libraries such as NextAuth and can be retreived automatically. Alternatively, you will need to parse the URL parameters for the code
key and value. (e.g. https://yourdomain.site/api/auth/callback/nexusmods?code=usercodehere
.
For desktop apps, you may not have a web server to handle this. We recommend setting up a listener on the user's machine (e.g. localhost:1337
) then parsing the code
parameter by capturing requests to that local address (e.g. localhost:1337/callback?code=usercodehere
).
The application now has a code which means the user has authorized it, so we use that code to request the JSON Web Token (JWT) which includes the user's Access Token, Refresh Token and Access Token Lifetime.
To exchange the code for tokens, we send a request to https://users.nexusmods.com/oauth/token
. The body of the request must have the following:
Key | Value | Notes |
---|---|---|
grant_type | "authorization_code" |
|
redirect_uri | {REDIRECT_URI} |
The redirect URI used by your application |
scope | '' |
The scopes your application requires. Use an empty string for no scopes. |
client_id | {CLIENT_ID} |
Your client ID. |
code | {CODE} |
The code you want to exchange for tokens. |
code_verifier | {CODE_VERIFIER} |
Required for public apps only. This is the original, uncoded UUID that you generated at the start. |
client_secret | {CLIENT_SECRET} |
Required for private apps only. |
const getToken = async code => {
try {
const request = new URLSearchParams({
grant_type: "authorization_code",
redirect_uri: redirectUri,
client_id: clientId,
code,
code_verifier: codeVerifier
});
const url = `https://users.nexusmods.com/oauth/token`;
const res = await fetch( url, {
method: 'POST',
body: request,
headers: {
'content-type': 'application/x-www-form-urlencoded'
}
});
if (!res.ok) {
throw new Error(`Network Error - HTTP ${res.status} - ${res.status_text}`)
}
return res.data;
} catch ( err ) {
console.log( "error getting token", err );
throw err;
}
};
The final step is now to decode the JWT using the Nexus Mods Public Key (below). Make sure you include the linebreaks!
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhKHxCWOeUy38S3UOBOB11SNd/
wyL9TVvzxePkEsZb4fEVGp0U5MEcDcJgXUo/fZOYTUFMX7ipvCC7sbsyKpJ0xZ/M
l5zXMBcI03gu6p1TvG+eL0xEk6X8LD+t+GbzH9EY58bZ8kOLEx4lbAX3fNYhMhbh
HJra9ZVW2QdgHoDV6wIDAQAB
-----END PUBLIC KEY-----
const jwt = require('jsonwebtoken');
// Use a JWT Library to decode the token
const decoded = jwt.verify(token.access_token, nexusPublicKey);
The final object will look something like this:
{
"application_id": 100,
"exp": 1754411198,
"iat": 1754389598,
"jti": "some-code",
"sub": "12345",
"user": {
"group_id": 1,
"id": 12345,
"joined": 1419531134,
"membership_roles": [
"member",
"supporter",
"premium",
"lifetimepremium"
],
"other_group_ids": "",
"permissions": {},
"premium_expiry": 0,
"username": "TestAccount"
},
"iss": "nexus-user-service"
}
When interacting with the API, both the V1 and V2 APIs will accept the user's current Access Token in the Authorization header (e.g. { "Authorization": "Bearer tokengoeshere" }
).
Before making a request with the access token, it is good practice to check the token expiry first. The expiry can be calculated by taking the DateTime that the token was generated and added the tokens.expires_in
value to it.
If the token has expired, the refresh token (tokens.refresh_token
) can be used to request a new access token. If this request fails with a HTTP error code in the 400 range, this likely means the user has revoked the application's access to their account and they user should be considered logged out.
// This calculation should be made and stored when initially recieving the token.
tokens.expires_at = Date.now() + (tokens.expires_in * 1000);
// Before making a request, validate if the token has expired.
async function getAccesToken(tokens) {
// The token is still valid, so we don't need to refresh it.
if (Date.now() < tokens.expires_at) return tokens.access_token;
else {
const url = 'https://users.nexusmods.com/oauth/token';
const body = new URLSearchParams({
client_id: NEXUS_OAUTH_ID,
client_secret: NEXUS_OAUTH_SECRET, // For private apps
grant_type: 'refresh_token',
refresh_token: tokens.refresh_token,
});
const response = await fetch(url, {
body,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
if (response.ok) {
const newTokens = await response.json();
// Save the updated tokens somehow
tokens.expires_at = Date.now() + (tokens.expires_in * 1000);
return newTokens.access_token;
}
else throw new Error(`Could not refresh Nexus Mods access token: [${response.status}] ${response.statusText}`);
}
};