Oauth2 How To

Thanks to @anoek for helping me figure this out. We got the Oauth2 connection working! This should be the preferred method for logging in over getting a user’s username and password like almost all 3rd party apps currently do.

I apologize if my settings are not perfect or written well. I had to ChatGPT part of this and even I don’t understand how everything works lol~~ My hope is others will use this as a start and write something better to share with others.

Firstly you will need to register your application here: Play Go at online-go.com! | OGS

The redirect uri is where OGS will redirect the user after logging in.

Secondly, you will need to get your Client id from there. You will need to send this to OGS to get your code.

Next, you need to actually send the user to OGS Oauth2 link Play Go at online-go.com! | OGS with the proper parameters. Here is an example of what the url should look like.

image

At the top of my script I list the variable links that you will need to use.

image

Important!! The urls must match EXACTLY this included the / at the end of /token/. Your redirect_uri also must match exactly to what you input in your application settings above. Including the /

This is the part that comes next, I don’t fully understand it but it creates some of the parameters needed. (They need unique parameters to match the callbacks.)

Next I check the url for the code. This code will be given from OGS, but on the first time the page loads I don’t have it yet. So I handle the case where the code doesn’t exist by creating the parameters and sending everything to ogs.

// --- Check for redirect params ---
      const params = new URLSearchParams(window.location.search);
      const code = params.get("code");
      const returnedState = params.get("state");

      if (!code) {
        // === Step 1: Start Login ===
        const randomArray = new Uint8Array(32);
        crypto.getRandomValues(randomArray);
        const verifier = base64URLEncode(randomArray);
        const challenge = base64URLEncode(await sha256(verifier));
        const state = crypto.randomUUID();

        localStorage.setItem("ogs_verifier", verifier);
        localStorage.setItem("ogs_state", state);

        // The important bit. The URL.
        // Note we are sending it to the auth_url /authorize above.
        const url = `${auth_url}?client_id=${client_id}&redirect_uri=${encodeURIComponent(redirect_uri)}&response_type=code&code_challenge=${challenge}&code_challenge_method=S256&state=${state}`;
        window.location.href = url;

      }

Next, the user should see something like this.

Once they Authorize your app, they will be sent to your redirect_uri with a code and state in the parameters.

image

Now you need to trade the code with OGS to get the users access_token and refresh_token. The access_token is what you will need to do all the call to ogs to play moves, accept games, ect…

Send the code to /token/ url. Don’t forget the /

image

 else { // I have the code in the url
        // === Step 2: Handle Redirect ===
        const verifier = localStorage.getItem("ogs_verifier");
        const originalState = localStorage.getItem("ogs_state");

        // ✅ Extra guard to ensure we have what we need
        if (!verifier || !originalState) {
          console.error("Missing verifier or state — user may have refreshed after login.");
          return;
        }

        if (returnedState !== originalState) {
          console.error("State mismatch — possible CSRF attack.");
          return;
        }

        const bodyData = new URLSearchParams({
          grant_type: "authorization_code",
          code,
          redirect_uri,
          client_id,
          code_verifier: verifier
        });

        try {
          console.log("Sending POST to:", token_url);
            console.log('Body Data: ' + bodyData.toString());
          const response = await fetch(token_url, {
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: bodyData.toString()
          });

          console.log("Response status:", response.status);
          const data = await response.json();

          // Once I have the token I handle it however I want.
          // In my case, I save it to the user's local storage.
          if (typeof bubble_fn_ogsToken === "function") {
              
              let bubble = {
              	'output1': data.access_token,
              	'output2': data.expires_in,
                'output3': data.refresh_token
              };
          	bubble_fn_ogsToken(bubble);
          }

        } catch (err) {
          console.error("Error exchanging code for token:", err);
        }
      }
    })();

After all this you should have the access_token and refresh_token on the user’s device and should be able to do everything you need to on OGS.

For reference, this is my whole script below.


    (async function() {
      const client_id = "My_Client_id"; // Put your client id here.
      const redirect_uri = "https://tsumegodragon.com/ogs-login";
      const token_url = "https://online-go.com/oauth2/token/";
      const auth_url = "https://online-go.com/oauth2/authorize";

      // --- Helper functions ---
      function base64URLEncode(buffer) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
          .replace(/\+/g, "-")
          .replace(/\//g, "_")
          .replace(/=+$/, "");
      }

      async function sha256(str) {
        const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
        return buf;
      }

      // --- Check for redirect params ---
      const params = new URLSearchParams(window.location.search);
      const code = params.get("code");
      const returnedState = params.get("state");

      if (!code) {
        // === Step 1: Start Login ===
        const randomArray = new Uint8Array(32);
        crypto.getRandomValues(randomArray);
        const verifier = base64URLEncode(randomArray);
        const challenge = base64URLEncode(await sha256(verifier));
        const state = crypto.randomUUID();

        localStorage.setItem("ogs_verifier", verifier);
        localStorage.setItem("ogs_state", state);

        const url = `${auth_url}?client_id=${client_id}&redirect_uri=${encodeURIComponent(redirect_uri)}&response_type=code&code_challenge=${challenge}&code_challenge_method=S256&state=${state}`;
        window.location.href = url;

      } else {
        // === Step 2: Handle Redirect ===
        const verifier = localStorage.getItem("ogs_verifier");
        const originalState = localStorage.getItem("ogs_state");

        // ✅ Extra guard to ensure we have what we need
        if (!verifier || !originalState) {
          console.error("Missing verifier or state — user may have refreshed after login.");
          return;
        }

        if (returnedState !== originalState) {
          console.error("State mismatch — possible CSRF attack.");
          return;
        }

        const bodyData = new URLSearchParams({
          grant_type: "authorization_code",
          code,
          redirect_uri,
          client_id,
          code_verifier: verifier
        });

        try {
          console.log("Sending POST to:", token_url);
            console.log('Body Data: ' + bodyData.toString());
          const response = await fetch(token_url, {
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: bodyData.toString()
          });

          console.log("Response status:", response.status);
          const data = await response.json();

          if (typeof bubble_fn_ogsToken === "function") {
              
              let bubble = {
              	'output1': data.access_token,
              	'output2': data.expires_in,
                'output3': data.refresh_token
              };
          	bubble_fn_ogsToken(bubble);
          }

        } catch (err) {
          console.error("Error exchanging code for token:", err);
        }
      }
    })();

As I said, this probably isn’t the cleanest method as I am no expert. Someone else should be able to make a cleaner version and will hopefully share it with others as well.

With all that being said, I hope this helps everyone who needed to login with Oauth2!

7 Likes

Thanks for sharing this! It’s been super helpful for my own app to know this is possible!

To add some additional flavor from my own new implementation: I ended up using the library GitHub - capacitor-community/generic-oauth2: Generic Capacitor OAuth 2 client plugin. Stop the war in Ukraine! , which won’t necessarily be helpful to anyone else unless you specifically happen to be writing a mobile app using JS/TS and Capacitor (a native mobile app wrapper), but it is useful to know that OGS’ OAuth implementation is spec-compliant once you have the correct details.

// clientId = client ID fetched from http://[beta.]online-go.com/oauth2/applications/
// host = either "https://online-go.com" or "https://beta.online-go.com"

const options: OAuth2AuthenticateOptions = {
    appId: clientId,
	authorizationBaseUrl: `${host}/oauth2/authorize`, // Endpoint that returns a code
	responseType: "code",
	redirectUrl,
	pkceEnabled: true, // OGS doesn't enforce this, but you should use PKCE from a mobile or client-only app. The library I'm using handles generating a code challenge and verifier for you
	accessTokenEndpoint: `${host}/oauth2/token/`, // Endpoint that takes a code and gives an access token
	web: {
		sendCacheControlHeader: false, // OGS enforces CORS rules on cache control headers, which it possibly should change but at least this library exposes a workaround
	},
}

const response = await GenericOAuth2.authenticate(options) // Triggers a browser window to open
// response.access_token_response contains the full access token response with access token, refresh token, expiry, etc

I’m not using my library for refreshing tokens, since its refresh token functionality doesn’t work on web (and I use a web version of my app for debug purposes), so I have my own refresh token fetcher function:

async function getRefreshToken(auth: Auth) {
    // Client ID and secret fetched from the OGS OAuth Applications page.
    // Note that the secret is only shown at initial app creation, on subsequent pageloads you get an encrypted version
	const clientId = shouldUseBetaServer()
		? import.meta.env.VITE_OGS_BETA_CLIENT_ID
		: import.meta.env.VITE_OGS_CLIENT_ID
	const clientSecret = shouldUseBetaServer()
		? import.meta.env.VITE_OGS_BETA_CLIENT_SECRET
		: import.meta.env.VITE_OGS_CLIENT_SECRET

	if (!auth.refreshToken) {
		throw new Error("[AUTH] Missing OGS credentials")
	}

    // As in the previous example, getHost() resolves to either https://online-go.com or https://beta.online-go.com
	const response = await fetch(`${getHost()}/oauth2/token/`, {
		method: "POST",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: new URLSearchParams({
			grant_type: "refresh_token",
			refresh_token: auth.refreshToken,
			client_id: clientId,
			client_secret: clientSecret,
		}),
	})
	if (!response.ok) {
		throw new Error("[AUTH] Failed auth: " + response.statusText)
	}
	const data = await response.json()

	return {
		accessToken: data.access_token,
		refreshToken: data.refresh_token,
		expiry: new Date(Date.now() + data.expires_in),
	}
}

A gotcha for me is that the OGS OAuth app does not allow redirect URIs to contain arbitrary schemes for mobile deep-linking. (e.g. tenuki-oauth:// in my case). I have my redirect URI set to a publicly-available website that consists of the following HTML:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Logging in...</title>
</head>
<body>
    <p>Completing login...</p>
    <script>
        const params = new URLSearchParams(window.location.search);
        const code = params.get('code');
        const error = params.get('error');
        const state = params.get('state');
        
        if (code) {
            window.location.href = 'tenuki.oauth://?code=' + code + '&state=' + state;
        } else if (error) {
            window.location.href = 'tenuki.oauth://?error=' + error + '&state=' + state;
        }
    </script>
</body>
</html>

So the OAuth app happily redirects to my HTTP OAuth page, which in turn immediately redirects to a native link my app can handle.

If I had two asks for OGS based on this, it would be (in order):

  • Allow redirect URIs in the OAuth application manager to contain arbitrary/non-standard URI schemes
  • Change CORS settings to allow cache-control headers from arbitrary browser origins
1 Like