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 Developers Console
- Configuring our application
- Full Example
- Testing
- Additional resources
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.
127.0.0.1 me.mydomain.com
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/hosts
orsudo 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:
Inside API & Services
→ Credentials
, there is a Create Credentials
button with an OAuth Client Id
option:
But first, we have to Configure the OAuth consent screen:
Now we can create the OAuth credentials, with the following information:
- Authorized JavaScript origins:
http://me.mydomain.com:8080
- Authorized redirect URIs:
http://me.mydomain.com:8080/login
Press the Create
button.
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
Configuring our application
First we have to define the settings for our OAuth provider. We have to replace the clientId
and clientSecret
with the values obtained from the previous step. Depending on what we need from the user, we can adjust the defaultScopes
list 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:
val googleOauthProvider = OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://www.googleapis.com/oauth2/v3/token",
requestMethod = HttpMethod.Post,
clientId = "xxxxxxxxxxx.apps.googleusercontent.com",
clientSecret = "yyyyyyyyyyy",
defaultScopes = listOf("profile") // no email, but gives full name, picture, and id
)
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
:
install(Authentication) {
oauth("google-oauth") {
client = HttpClient(Apache)
providerLookup = { googleOauthProvider }
urlProvider = { redirectUrl("/login") }
}
}
private fun ApplicationCall.redirectUrl(path: String): String {
val defaultPort = if (request.origin.scheme == "http") 80 else 443
val hostPort = request.host()!! + request.port().let { port -> if (port == defaultPort) "" else ":$port" }
val protocol = request.origin.scheme
return "$protocol://$hostPort$path"
}
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.
class MySession(val userId: String)
authenticate("google-oauth") {
route("/login") {
handle {
val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
?: error("No principal")
val json = HttpClient(Apache).get<String>("https://www.googleapis.com/userinfo/v2/me") {
header("Authorization", "Bearer ${principal.accessToken}")
}
val data = ObjectMapper().readValue<Map<String, Any?>>(json)
val id = data["id"] as String?
if (id != null) {
call.sessions.set(MySession(id))
}
call.respondRedirect("/")
}
}
}
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
val googleOauthProvider = OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://www.googleapis.com/oauth2/v3/token",
requestMethod = HttpMethod.Post,
clientId = "xxxxxxxxxxx.apps.googleusercontent.com", // @TODO: Remember to change this!
clientSecret = "yyyyyyyyyyy", // @TODO: Remember to change this!
defaultScopes = listOf("profile") // no email, but gives full name, picture, and id
)
class MySession(val userId: String)
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
install(WebSockets)
install(Sessions) {
cookie<MySession>("oauthSampleSessionId") {
val secretSignKey = hex("000102030405060708090a0b0c0d0e0f") // @TODO: Remember to change this!
transform(SessionTransportTransformerMessageAuthentication(secretSignKey))
}
}
install(Authentication) {
oauth("google-oauth") {
client = HttpClient(Apache)
providerLookup = { googleOauthProvider }
urlProvider = {
redirectUrl("/login")
}
}
}
routing {
get("/") {
val session = call.sessions.get<MySession>()
call.respondText("HI ${session?.userId}")
}
authenticate("google-oauth") {
route("/login") {
handle {
val principal = call.authentication.principal<OAuthAccessTokenResponse.OAuth2>()
?: error("No principal")
val json = HttpClient(Apache).get<String>("https://www.googleapis.com/userinfo/v2/me") {
header("Authorization", "Bearer ${principal.accessToken}")
}
val data = ObjectMapper().readValue<Map<String, Any?>>(json)
val id = data["id"] as String?
if (id != null) {
call.sessions.set(MySession(id))
}
call.respondRedirect("/")
}
}
}
}
}.start(wait = true)
}
private fun ApplicationCall.redirectUrl(path: String): String {
val defaultPort = if (request.origin.scheme == "http") 80 else 443
val hostPort = request.host()!! + request.port().let { port -> if (port == defaultPort) "" else ":$port" }
val protocol = request.origin.scheme
return "$protocol://$hostPort$path"
}
Testing
You can provide a test HttpClient for testing OAuth.
Additional resources
- Google OAuth playground: https://developers.google.com/oauthplayground/
- List of available google oauth scopes: https://developers.google.com/identity/protocols/googlescopes
- Example with several oauth providers: https://github.com/ktorio/ktor-samples/blob/master/feature/auth/src/io/ktor/samples/auth/OAuthLoginApplication.kt