Skip to main content

Discord OAuth

The discord-oauth module logs users in with Discord in a Javalin web app. It runs the OAuth2 authorization code flow, refreshes tokens before they expire and calls the Discord API on the user's behalf.

implementation("dev.chojo.aether:discord-oauth:2.0.0")

Configuration​

DiscordOAuth holds the settings of your Discord application:

KeyDefaultMeaning
clientId""The client id of your Discord application.
clientSecret""Its client secret.
redirectUri""The callback URL. It must be listed as a redirect in the Discord developer portal.
cacheRetentionMinutes10How long user, guild and connection data stays cached.
scopesIDENTIFY, EMAIL, GUILDSThe scopes to ask for.
promptNONENONE skips the consent screen for users who agreed before, CONSENT always shows it.
discordOAuth:
clientId: "1234567890123456789"
clientSecret: secret
redirectUri: https://api.example.com/auth/callback
scopes: [IDENTIFY, EMAIL, GUILDS]

The class keeps its values in private fields. Read it with a mapper that can see private fields, for example Ocular.

Your service​

Extend ADiscordOAuthService. You implement two methods:

MethodWhat it should do
storeToken(user, token)Store the Discord token for the user and return a session token of your own. The user is sent back to your web app with it.
getExpiringTokens(instant)Return the stored tokens that expire before the given time.

Your stored token implements IOAuthToken: user id, access token, refresh token, scopes and expiry, plus update(TokenResponse) to save a refreshed token and delete() to remove it.

public class LoginService extends ADiscordOAuthService {
private final TokenRepository tokens;

public LoginService(DiscordOAuth config, TokenRepository tokens) {
super(config, "https://dashboard.example.com");
this.tokens = tokens;
}

@Override
public String storeToken(DiscordUser user, TokenResponse token) {
return tokens.save(user.id(), token).sessionToken();
}

@Override
protected List<? extends IOAuthToken> getExpiringTokens(Instant instant) {
return tokens.expiringBefore(instant);
}
}

LoginService loginService = new LoginService(config, tokens);
loginService.start();

The second constructor argument is the base URL of your web app, where users land after the login.

start() starts the token refresh: two minutes later and then every 60 minutes, every token that expires within the next hour is refreshed and saved with update. A token that can not be refreshed is deleted. The service does not call start() itself.

Routes​

Register the two handlers on your own routes. The callback route must match redirectUri:

app.get("/auth/login", loginService::startDiscordLogin);
app.get("/auth/callback", loginService::handleDiscordCallback);
  1. Your web app sends the user to /auth/login?state=/settings. The state parameter is the path to return to.
  2. startDiscordLogin sets an oauth_state cookie (HttpOnly, five minutes) and redirects to Discord.
  3. Discord sends the user to the callback. handleDiscordCallback checks the cookie, exchanges the code for a token, loads the user and calls storeToken.
  4. The user is redirected to your web app at the requested path with ?token=<your session token>.

Any error in the callback answers with status 400 and an ErrorResponseWrapper (see Common web).

Calling Discord​

client() returns the DiscordClient. Each call takes the user's IOAuthToken:

MethodNeeds scopeReturns
user(token)IDENTIFY (EMAIL for the address)DiscordUser with id and mail address.
guilds(token)GUILDSThe user's guilds as DiscordGuild. Empty without the scope.
connections(token)CONNECTIONSLinked accounts as Connection. Empty without the scope.
guildMember(token, guildId)GUILDS_MEMBERS_READThe user's Member in a guild, or null.
joinGuild(data, token, botToken, guildId)GUILDS_JOINAdds the user to a guild your bot is in. Returns the new Member, or empty when the user is a member already.
revokeToken(token)Revokes the token at Discord and drops its cached data.

user, guilds and connections are cached per access token for cacheRetentionMinutes. user, guilds, connections and joinGuild throw when Discord answers with an error.