Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Route header classes #4562

Open
wants to merge 6 commits into
base: auth-exchange
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 0 additions & 54 deletions firebase-authexchange/firebase-authexchange.gradle

This file was deleted.

49 changes: 49 additions & 0 deletions firebase-authexchange/firebase-authexchange.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
plugins {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing copyright notice here and elsewhere

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I had a script I wrote that would automatically add it to any missing files, but it doesn't auto-run. We should probably have some sort of check regarding that. Not anything that's enforced, since there are way too many exceptions, but maybe like a git hook or gha that looks at files added and lets you know if any of interest are missing copyright notices (.kt,.java,etc.,).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one, and it's correctly reporting this :-)

https://github.com/firebase/firebase-android-sdk/actions/runs/3917153439

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL, well I'll be paying more attention to that one for sure

id("firebase-library")
kotlin("android")
kotlin("plugin.serialization") version "1.7.20"
}

firebaseLibrary {
publishSources = true
}

android {
val targetSdkVersion : Int by rootProject

compileSdk = targetSdkVersion
defaultConfig {
minSdk = 16
targetSdk = targetSdkVersion
multiDexEnabled = true
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

sourceSets {
getByName("main") {
java.srcDirs("src/main/kotlin")
}
getByName("test") {
java.srcDirs("src/test/kotlin")
}
}

testOptions.unitTests.isIncludeAndroidResources = true
}

dependencies {
daymxn marked this conversation as resolved.
Show resolved Hide resolved
implementation(project(":firebase-common"))
implementation(project(":firebase-common:ktx"))
implementation(project(":firebase-components"))
implementation(project(":firebase-authexchange-interop"))

implementation(libs.androidx.annotation)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
implementation(libs.kotlin.stdlib)


testImplementation(libs.androidx.test.core)
testImplementation(libs.truth)
testImplementation(libs.junit)
testImplementation(libs.robolectric)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.google.firebase.authexchange.network

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Defines what an HTTP request may look like when interacting with the backend API for
rlazo marked this conversation as resolved.
Show resolved Hide resolved
* [FirebaseAuthExchange][com.google.firebase.authexchange.FirebaseAuthExchange].
*/
@Serializable internal sealed interface Request
rosalyntan marked this conversation as resolved.
Show resolved Hide resolved

/** Defines what an HTTP response may look like when submitting a [Request]. */
@Serializable internal sealed interface Response

/**
* OIDC Credential.
*
* Useful for apps with external login already set-up, otherwise known as a headless OIDC flow.
Comment on lines +16 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is an OIDC credential, would it make sense to call the class that? Not sure if implicit credentials is the right naming.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be comfortable with that, although the property name would still be implicitCredentials in the API call, because that's what the API expects.

I can't link it here since it's an internal link- but if you look at the bug linked in this PR (referenced in the OP), and go to the first link in the comments of that bug, you can see all the proto defs for the API. These definitions and their names are derived from that- since these are all just data classes for the API- intended to be encoded to JSON via @Serializable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I forgot we can specify a @SerialName for encoding and decoding, so we would call it something else but it gets translated to implicitCredentials when it's sent. But seeing as this is ONLY for the API, and shouldn't be used beyond that- I think it's totally fine keeping it as is. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an scenario where more details help :-) You could change the first line of the description to "Credentials returned by an OIDC implicit flow" Then the name would sound more fitting. No need to use @SerialName

*
* @property idToken JWT encoded OIDC token returned from a third party provider
*/
@Serializable internal data class ImplicitCredentialsP(val idToken: String)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the P suffix stand for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protobuf, implying that these data classes are representations of gRPC related data (that is, they're raw classes that shouldn't be used directly, and are intended to only be used by the network layer). I'm not quite sure where I picked this up from, but I've used this naming convention by habit in all of my personal gRPC projects. Helps more easily difference between the network and application layer.

Although if you're against it, I'm fine with removing it:)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be consistent with the rest of our code, I'd rathe remove it.

That being said, the discussion of the value and usage of suffixes in code naming is a long and interesting one for the future :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to removing the P suffix. If you really want to add a suffix for clarity, perhaps just fully putting Proto instead? Unless the P shortening is a Kotlin convention.


/**
* Auth Token that can be used to access certain Firebase Services.
*
* This is merely the proto type for the network layer, and is completely separate from the user
* facing [AuthExchangeToken][com.google.firebase.authexchange.AuthExchangeToken].
*
Comment on lines +27 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by a proto type in this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in it's a data class used for communicating via protobuf APIs (the backend for auth echange). Although I do think the phrasing gives off the wrong impression. Thoughts on this instead:

 * Not to be confused with [AuthExchangeToken][com.google.firebase.authexchange.AuthExchangeToken],
 * this is intended only for network requests.

Or maybe just removing it entirely?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 your suggested rewording.

* @property accessToken signed JWT containing claims that identify a user
* @property timeToLive the duration from the time this token is minted until its expiration
*/
@Serializable
internal data class AuthExchangeTokenP(
val accessToken: String,
@SerialName("ttl") val timeToLive: String
)

/**
* Request header for the `/ExchangeInstallationAuthToken` endpoint.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a header or the actual request?

here and below

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So nothing here actually implements the requests themselves. These are all just headers, or type defs, of what the API expects. Skeletons you could say. Actual implementation and utilization of these classes will be elsewhere in the SDK.

The API is currently defined in protobuf in G3, and all these classes provide a type-safe kotlin variant of the API as it is defined. Additionally, we can use kotlin's serialization, and immediately encode classes to JSON requests for the backend, and vise versa for JSON responses from the backend.

Just a little quick example (psuedo code):

data class GetPersonRequestP(val name: String)
data class GetPersonResponseP(val person: Person?, val error: String?)

data class Person(val name: String, age: Int, phoneNumber: String) 

fun getPerson() {
  val myQuery = GetPersonRequestP("Rodrigo")

  val response: GetPersonResponseP = googleServer:GetRequest<GetPersonRequestP>(myQuery)

  response?.person?.let {
    println("Rodrigo is ${it.age} years old!")
  }
}

All subclasses of Request are definitions for what individual API endpoints expect in their request call.

All subclasses of Response are definitions for what API endpoints return in their response to request calls.

Anything else is just extra data types that either need to be arguments in a call to a certain API, or are apart of a response.

Does that answer your question? Or did I misunderstand what you were asking?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All below are either Request or Response subclasses, and they are named as such, but the doc says "headers" which is different. A header is metadata about the request, but these classes contain the actual content of the requests/responses.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All below are either Request or Response subclasses, and they are named as such, but the doc says "headers" which is different. A header is metadata about the request, but these classes contain the actual content of the requests/responses.

They contain the content sure, but only in the sense that it's a data class and doesn't have any content. I think C headers- where data types and function signatures are declared in header files, and the source file implements said methods.

Although, what do you think would be a better name? We could also always just split it up into two separate files; request.kt and response.kt.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to "request header" implying network request headers, for example the x-firebase-appcheck header.

Though I'm not sure I understand what you mean when you say the actual implementation of these classes will exist elsewhere -- from my understanding of a Kotlin data class, it is essentially like a struct, which is fully instantiable (i.e. it's not like a Java interface where you need to implement the interface before it can be used). Also, are these data classes not implementations of the Request interface already?

Copy link
Member Author

@daymxn daymxn Jan 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from my understanding of a Kotlin data class, it is essentially like a struct

You're right on the money

what you mean when you say the actual implementation of these classes

Implementation was probably the wrong phrasing, I more-so meant the usage of these classes occur elsewhere- and this isn't intended to provide any logic

Also, are these data classes not implementations of the Request interface already?

They are, I just meant splitting the files for naming & organization sake

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see -- in terms of naming clarity, I think you can just remove the term "header" from the documentation and leave the rest of the comment the same.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

*
* @see ExchangeTokenResponseP
*
* @property token relative resource name of the audience project and location
* @property installationAuthToken the installation token issued to the app
*/
@Serializable
internal data class ExchangeInstallationAuthTokenRequestP(
val token: String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token field is unnecessary as part of the request object (here and in all the requests). In the backend, the token field is extracted from the request URL.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I see. Is it the key param, or is it actually called token?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by key param? For additional context -- here is the request path. Note the token= portion of the request path -- that is where the token field is populated from.

val installationAuthToken: String
) : Request

/**
* Request header for the `/ExchangeCustomToken` endpoint.
*
* @see ExchangeTokenResponseP
*
* @property token relative resource name of the audience project and location
* @property customToken a custom JWT token signed with the developer's credentials
*/
@Serializable
internal data class ExchangeCustomTokenRequestP(val token: String, val customToken: String) :
Request

/**
* Request header for the `/ExchangeOidcToken` endpoint.
*
* @see ExchangeOidcTokenResponseP
*
* @property token relative resource name of the audience project and location
* @property providerId the display name or id of the OIDC provider
* @property implicitCredentials JWT token from the OIDC provider, provided by the developer
*/
@Serializable
internal data class ExchangeOidcTokenRequestP(
val token: String,
val providerId: String,
val implicitCredentials: ImplicitCredentialsP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ExchangeOidcTokenRequest can take either implicit credentials or auth code credentials (see the API proto). Is there a way to represent this as a oneof?

Copy link
Member Author

@daymxn daymxn Jan 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, although do we use it on our end? I initially had the following comment:

/**
 * While this isn't the only OIDC flow that the API supports, it's the only flow that the mobile SDK
 * supports- so there is no base class for credentials.
 */

Because I was under the impression we don't utilize AuthCodeCredentials on the SDK?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auth code OIDC flow is the "headful" flow (detailed here).

) : Request

/**
* Response header for endpoints that just expect an [AuthExchangeTokenP].
*
* @see ExchangeCustomTokenRequestP
* @see ExchangeInstallationAuthTokenRequestP
*
* @property token auth token returned by the backend
*/
@Serializable internal data class ExchangeTokenResponseP(val token: AuthExchangeTokenP) : Response

/**
* Response header for the `/ExchangeOidcToken` endpoint.
*
* @see ExchangeOidcTokenRequestP
*
* @property authExchangeToken auth token returned by the backend
* @property oidcIdToken OAuth id token received from the OIDC provider
* @property oidcRefreshToken optional OAuth refresh token received from the OIDC provider
*/
@Serializable
internal data class ExchangeOidcTokenResponseP(
val authExchangeToken: AuthExchangeTokenP,
val oidcIdToken: String,
val oidcRefreshToken: String? = null
) : Response