controllers.DiscourseAuth.java Source code

Java tutorial

Introduction

Here is the source code for controllers.DiscourseAuth.java

Source

/*
 * MyTake.org
 *
 *  Copyright 2017 by its authors.
 *  Some rights reserved. See LICENSE, https://github.com/mytakedotorg/mytakedotorg/graphs/contributors
 */
package controllers;

import static db.Tables.ACCOUNT;

import auth.AuthUser;
import com.google.inject.Binder;
import com.typesafe.config.Config;
import db.tables.records.AccountRecord;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
import java2ts.Routes;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
import org.jooby.Env;
import org.jooby.Jooby;
import org.jooby.Request;
import org.jooby.Results;
import org.jooq.DSLContext;
import org.postgresql.util.Base64;

/**
 * SSO for Discourse, the spec is here: https://meta.discourse.org/t/official-single-sign-on-for-discourse-sso/13045
 * 
 * Thanks to the following examples.  They didn't "drop right in", but they were a helpful start.
 *
 * https://github.com/KevinWorkman/StaticVoidGames/blob/master/StaticVoidGames/src/main/java/com/StaticVoidGames/spring/config/LoginSuccessHandler.java
 * https://meta.discourse.org/t/sso-example-for-jsp/22786
 */
public class DiscourseAuth implements Jooby.Module {
    private static final String DISCOURSE = "https://meta.mytake.org";
    private static final String DISCOURSE_SSO = DISCOURSE + "/session/sso_login";
    protected static final String SECRET_KEY = "discourse.secret";

    /** Appends the user's info to the Nonce, fields from. */
    protected String appendUserInfoToNonce(Request req, String nonce) throws UnsupportedEncodingException {
        // find logged-in user
        AuthUser auth = AuthUser.auth(req);
        AccountRecord account;
        try (DSLContext dsl = req.require(DSLContext.class)) {
            account = dsl.selectFrom(ACCOUNT).where(ACCOUNT.ID.eq(auth.id())).fetchOne();
        }
        // return the logged-in user
        String name = Optional.ofNullable(account.getName()).orElse(account.getUsername());
        return nonce + "&name=" + urlEncode(name) + "&username=" + urlEncode(account.getUsername()) + "&email="
                + urlEncode(account.getEmail()) + "&external_id=" + urlEncode(account.getId().toString());
    }

    @Override
    public void configure(Env env, Config conf, Binder binder) throws Throwable {
        if (!conf.hasPath(SECRET_KEY)) {
            return;
        }
        byte[] keyBytes = conf.getString(SECRET_KEY).getBytes(StandardCharsets.UTF_8);
        SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA256");
        env.router().get(Routes.API + "/discourseAuth", req -> {
            // validate request from server
            String payload = urlDecode(req.param("sso").value());
            String sig = req.param("sig").value();
            if (!checksum(key, payload).equals(sig)) {
                throw new IllegalArgumentException("Invalid signature");
            }
            // decode the nonce
            String nonce = decodeBase64(payload);
            String response = appendUserInfoToNonce(req, nonce);
            String response64 = encodeBase64(response);
            if (!response64.endsWith("\n")) {
                response64 += "\n";
            }
            return Results.redirect(
                    DISCOURSE_SSO + "?sso=" + urlEncode(response64) + "&sig=" + checksum(key, response64));
        });
    }

    protected static String urlEncode(String input) throws UnsupportedEncodingException {
        return URLEncoder.encode(input, StandardCharsets.UTF_8.name());
    }

    protected static String urlDecode(String input) throws UnsupportedEncodingException {
        return URLDecoder.decode(input, StandardCharsets.UTF_8.name());
    }

    private static String checksum(SecretKeySpec key, String macData)
            throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(key);
        byte[] doFinal = mac.doFinal(macData.getBytes(StandardCharsets.UTF_8));
        return Hex.encodeHexString(doFinal);
    }

    private static String decodeBase64(String input) throws UnsupportedEncodingException {
        return new String(Base64.decode(input), StandardCharsets.UTF_8);
    }

    private static String encodeBase64(String input) throws UnsupportedEncodingException {
        return Base64.encodeBytes(input.getBytes(StandardCharsets.UTF_8));
    }
}