Guides: How to implement an OAuth login with Google

In this guide we are going to implement a login using OAuth.

This is an advanced tutorial and it assumes you have some basic knowledge about Ktor,so you should follow the guide about making a Website first.

Table of contents:

Creating a host entry pointing to 127.0.0.1

Google’s OAuth requires redirect URLs that can’t be IP addresses or localhost.So for development purposes we will need a proper host pointing to 127.0.0.1.It is not required that this host be accessible from outside our computer, so we can just set up for local host.There is a public domain http://lvh.me/ pointing to localhost/127.0.0.1, but you might want to provide yourown host locally for security reasons.

For this, you can add an entry in the hosts file) of your machine.

For this guide we are going to associate me.mydomain.com to 127.0.0.1, but you can change it according to your needs,as long as it is like a public top-level domain (.com, .org…) or has at least two components.

  1. 127.0.0.1 me.mydomain.com

Google OAuth - 图1

The structure of this file is simple: # character for comments,and each non empty and non-comment line, should contain an IP address followedby several host names separated by spaces or tabs.

MacOS/Linux

In MacOS and Linux (Unix) computers, you can find the host file in /etc/hosts. You will need root access to edit it.

sudo nano /etc/hostsorsudo vi /etc/hosts

Windows

In Windows, the host file is held at %SystemRoot%\System32\drivers\etc\hosts. You will need admin privilegesto edit this file. For example, you can use Notepad++ opened as administrator.

You can also paste %SystemRoot%\System32\drivers\etc in the Windows Explorer the path and then right clickin the hosts file to edit it. The structure of this file is the same as MacOS/Linux.

Google Developers Console

To be able to use OAuth with any provider, you will need a public clientId, and a private clientSecret.In the case of Google login, you can create it using the Google Developers Console:https://console.developers.google.com/

First you have to create a new project in the developers console:

Google OAuth - 图2Google OAuth - 图3

Inside API & ServicesCredentials, there is a Create Credentials button with an OAuth Client Id option:

Google OAuth - 图4Google OAuth - 图5Google OAuth - 图6Google OAuth - 图7

But first, we have to Configure the OAuth consent screen:

Google OAuth - 图8Google OAuth - 图9

Now we can create the OAuth credentials, with the following information:

Press the Create button.

Google OAuth - 图10

You can change these values later, or add additional authorized URLs by editing the credentials.

You will see a modal dialog with the following:

OAuth client

  • Here is your client ID: xxxxxxxxxxx.apps.googleusercontent.com
  • Here is your client secret: yyyyyyyyyyy

Google OAuth - 图11

Configuring our application

First we have to define the settings for our OAuth provider. We have to replace the clientId and clientSecretwith the values obtained from the previous step. Depending on what we need from the user, we can adjust the defaultScopeslist to something else. The profile scope will have access to the user id, full name, and picture, but not to the email or anything else:

  1. val googleOauthProvider = OAuthServerSettings.OAuth2ServerSettings(
  2. name = "google",
  3. authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
  4. accessTokenUrl = "https://www.googleapis.com/oauth2/v3/token",
  5. requestMethod = HttpMethod.Post,
  6. clientId = "xxxxxxxxxxx.apps.googleusercontent.com",
  7. clientSecret = "yyyyyyyyyyy",
  8. defaultScopes = listOf("profile") // no email, but gives full name, picture, and id
  9. )

Remember to adjust the defaultScopes to just request what you really need for the sake of security, user privacy, and trust.

We also have to install the OAuth feature and configure it. We need to provide a HTTP client instance, a provider lookupwhere we determine the provider from the call (we don’t need to put logic here since we are just supporting Google for this guide) anda urlProvider giving the redirection URL that must match the one specified as authorized redirection in the Google Developers Console, in this case http://me.mydomain.com:8080/login:

  1. install(Authentication) {
  2. oauth("google-oauth") {
  3. client = HttpClient(Apache)
  4. providerLookup = { googleOauthProvider }
  5. urlProvider = { redirectUrl("/login") }
  6. }
  7. }
  8. private fun ApplicationCall.redirectUrl(path: String): String {
  9. val defaultPort = if (request.origin.scheme == "http") 80 else 443
  10. val hostPort = request.host()!! + request.port().let { port -> if (port == defaultPort) "" else ":$port" }
  11. val protocol = request.origin.scheme
  12. return "$protocol://$hostPort$path"
  13. }

Then we have to define the /login route that must be authenticated against our authentication provider.When no get parameters are passed to that URL, the authentication feature will hook the handler, and willredirect us to the OAuth Consent Screen from Google, and it will redirect us back to our /login route with thestatus and code arguments that will be used by the authentication provider to call back to Google to obtainan accessToken and attach a OAuthAccessTokenResponse.OAuth2 principal to our call. And this time,our handler will be executed.

We can retrieve that accessToken by getting the generated OAuthAccessTokenResponse.OAuth2 principal andaccessToken. Then we can use the https://www.googleapis.com/userinfo/v2/me URLwith our accessToken passed as Authorization Bearer, to get a JSON with the user information.You can check the contents of this JSON by using the Google OAuth playground.

In this case, once we get the User ID, we are going to store it in a session, and then redirect to another place.

  1. class MySession(val userId: String)
  2. authenticate("google-oauth") {
  3. route("/login") {
  4. handle {
  5. val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
  6. ?: error("No principal")
  7. val json = HttpClient(Apache).get<String>("https://www.googleapis.com/userinfo/v2/me") {
  8. header("Authorization", "Bearer ${principal.accessToken}")
  9. }
  10. val data = ObjectMapper().readValue<Map<String, Any?>>(json)
  11. val id = data["id"] as String?
  12. if (id != null) {
  13. call.sessions.set(MySession(id))
  14. }
  15. call.respondRedirect("/")
  16. }
  17. }
  18. }

We have to install the Session feature first. Check the Full Example for details:

The ID from the user information is a string that looks like a number. Remember that JSON does not define long types,and that in cases like Twitter or Google, that have tons and tons of users and entities, that ID could be greaterthan 31 bits for a signed integer or even than 51 bits of precision from a standard Double. As a rule of thumb you should always treat IDs and other number-like values as strings if you don’t needto do arithmetic with them.

Full Example

A simple embedded application would look like this:

OAuthApp.kt

  1. val googleOauthProvider = OAuthServerSettings.OAuth2ServerSettings(
  2. name = "google",
  3. authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
  4. accessTokenUrl = "https://www.googleapis.com/oauth2/v3/token",
  5. requestMethod = HttpMethod.Post,
  6. clientId = "xxxxxxxxxxx.apps.googleusercontent.com", // @TODO: Remember to change this!
  7. clientSecret = "yyyyyyyyyyy", // @TODO: Remember to change this!
  8. defaultScopes = listOf("profile") // no email, but gives full name, picture, and id
  9. )
  10. class MySession(val userId: String)
  11. fun main(args: Array<String>) {
  12. embeddedServer(Netty, port = 8080) {
  13. install(WebSockets)
  14. install(Sessions) {
  15. cookie<MySession>("oauthSampleSessionId") {
  16. val secretSignKey = hex("000102030405060708090a0b0c0d0e0f") // @TODO: Remember to change this!
  17. transform(SessionTransportTransformerMessageAuthentication(secretSignKey))
  18. }
  19. }
  20. install(Authentication) {
  21. oauth("google-oauth") {
  22. client = HttpClient(Apache)
  23. providerLookup = { googleOauthProvider }
  24. urlProvider = {
  25. redirectUrl("/login")
  26. }
  27. }
  28. }
  29. routing {
  30. get("/") {
  31. val session = call.sessions.get<MySession>()
  32. call.respondText("HI ${session?.userId}")
  33. }
  34. authenticate("google-oauth") {
  35. route("/login") {
  36. handle {
  37. val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
  38. ?: error("No principal")
  39. val json = HttpClient(Apache).get<String>("https://www.googleapis.com/userinfo/v2/me") {
  40. header("Authorization", "Bearer ${principal.accessToken}")
  41. }
  42. val data = ObjectMapper().readValue<Map<String, Any?>>(json)
  43. val id = data["id"] as String?
  44. if (id != null) {
  45. call.sessions.set(MySession(id))
  46. }
  47. call.respondRedirect("/")
  48. }
  49. }
  50. }
  51. }
  52. }.start(wait = true)
  53. }
  54. private fun ApplicationCall.redirectUrl(path: String): String {
  55. val defaultPort = if (request.origin.scheme == "http") 80 else 443
  56. val hostPort = request.host()!! + request.port().let { port -> if (port == defaultPort) "" else ":$port" }
  57. val protocol = request.origin.scheme
  58. return "$protocol://$hostPort$path"
  59. }

Testing

You can provide a test HttpClient for testing OAuth.

Additional resources