Authentication Setup with Kratos Hydra - UnlockedLabs/UnlockEdv2 GitHub Wiki

Oauth2/OIDC Authentication Setup for UnlockEdV2

UnlockEd uses Ory Hydra for an oauth2 server, and [Ory Kratos] for identity management.

The Ory REST API Documentation provides extensive resources about interacting with these services.

Because the Ory services are back-end only, we have integrated our own UI for user login, and oauth2 consent.

Oauth2 Clients (provider platforms, for which we need to authenticate our users into) are registered with us, and the information is stored in our database. When adding a Provider Platform, there is an option to Auto Register the provider, in which we have a provider specific setup that will be called using their API to add UnlockEd as an OIDC server/identity provider.

We handle the initial Oauth2 flow by redirecting our users as usual to the kratos browser endpoint: /self-service/login/browser where kratos initializes a login flow for the user and sets the appropriate cookies. Kratos is configured to then redirect the client to our /login page with the ?flow={flow_uuidv4} query parameter attached, and the CSRF token set in the cookie. When it is an oauth2 flow, there is a &login_challenge=... also appended to the URI.

When the user submits their login form, it is sent to our /api/login endpoint, which then forwards the submission to kratos, and attaches the necessary parameters to the POST /self-service/login Kratos API endpoint, adding the oauth2 challenge if it's found:

if form.Challenge != "" {
    // if there was an oauth2 code challenge, we append it to the kratos url
    url += "&login_challenge=" + form.Challenge
}

If the initial code challenge is successful, kratos will return a redirect_to parameter in the JSON body, which we then return to the client. If it is an Oauth2 flow, this will be configured to our /consent endpoint, with the ?consent_challenge=... query parameter included. If the user selects that they consent to log in to an external provider, they send us a POST request to /api/consent/accept with the challenge, and we submit this to Hydra's PUT /admin/oauth2/auth/requests/consent/accept endpoint on behalf of the user, with the appropriate request body that specifies information about the Oauth2 Token that will be built for the user.

func buildConsentBody(user *models.User) ([]byte, error) {
	body := make(map[string]interface{})
	body["remember"] = true
	body["grant_access_token_audience"] = []string{os.Getenv("APP_URL")}
	body["remember_for"] = 3600 * 24
	body["grant_scope"] = []string{"openid", "offline", "profile", "email"}
	body["session"] = map[string]interface{}{
		"access_token": map[string]interface{}{
			"scope": []string{"openid", "offline", "profile", "email"},
		},
		"id_token": map[string]string{
			// we use the username for all these fields, because different platforms
			// allow different field options by default, so we include a few just in case
			"nickname":           user.Username,
			"email":              user.Username,
			"preferred_username": user.Username,
			"locale":             "en",
		},
	}
	return json.Marshal(body)
}

Upon successful return, Hydra will include the final redirect_to parameter, which we again forward on to the client.

	redirectURI := response["redirect_to"].(string)
	respBody := map[string]string{
		"redirect_to": redirectURI,
	}
	return writeJsonResponse(w, http.StatusOK, respBody)

If the client is successfully registered with the correct parameters, this will be to the RedirectURI of the client. e.g. canvas-url.com/login/oauth2/callback and we will have a complete oauth2 login.