Deploying Vue & .NET with Google OAuth on GCP Cloud Run (vue-dotnet-oauth-gcp-basic)
This example demonstrates the design of programs utilizing Google OAuth for authentication under a decoupled front-end and back-end architecture. Furthermore, it introduces the process of wrapping the program into a Docker image, storing it in the GCP Artifact Registry, and subsequently deploying it to GCP Cloud Run.
- This document and the related code are stored on GitLab: vue-dotnet-oauth-gcp-basic
- The demonstration program is deployed on Google Cloud Run: DEMO
- Deploying Vue & .NET with Google OAuth on GCP Cloud Run (vue-dotnet-oauth-gcp-basic)
- Related Technologies
- Reference
- Directory Structure
- Frontend
- Backend
- Vite Proxy
- Establishing a Testing Environment with Containers
- Pushing Images to Google Artifact Registry
- Deployment to GCP Cloud
Related Technologies
- Frontend: Utilizes the Vue 3 framework with Vuetify as the UI kit. Implements OAuth 2 Authorization Code Grant and Implicit Grant using yobaji/vue3-google-login for Google authentication.
- Backend: Developed using the .NET Core 6.0 framework, and calls Google APIs to retrieve userinfo.
Reference
Directory Structure
vue-dotnet-oauth-example
├── oauth-backend
│ └── oauth-google
└── oauth-frontend
└── oauth-app
Frontend
Setting Up a Dev Container for Vue3 Development
Please set up a Dev Container development environment under the oauth-frontend directory:
- In VS Code, press F1
- Choose ‘Dev Containers: Open Folder in Container…’
- Select: ‘Vue community’ (Develop an application with Vue.js, includes everything you need to get up and running.)
- Choose Node.js version: 18
- Select additional features to install: (skip)
- Then waiting “Adding Dev Container Configuration Files…” for couple menutes.
Modify git config
The backend of this example was developed in Windows using Visual Studio 2022, while the frontend was developed in a Linux operating system using the Dev Container in VS Code. To avoid differences in the end-of-line (EOL) character (CRLF in Windows, LF in Linux), it is necessary to adjust the git settings. The following command, run in the Dev Container, can adjust the EOL character to be consistently CRLF.
git config --global core.autocrlf true
Creating a Project with Vuetify 3
The simplest way to set up a Vue3 + Vuetify3 project is to execute the ‘create vuetify’ command:
yarn create vuetify
# Project name: oauth-app
# ✔ Which present would you like ti inatll?
# ✔ > Default (Vuetify)
# Base (Vuetify, VueRouter)
# Essentials (Vuetify, VueRouter, Pinia)
# Custom (Choose your features)
# ✔ Use TypeScript? Yes
# ✔ Would you like to install dependencies with yarn, npm, or pnpm? yarn
cd oauth-app
yarn dev
Vite Polling Configuration
You might encounter an issue when running your application inside a dev containerized environment where file changes are not detected automatically due to file system events not being properly propagated. This could prevent automatic recompilation or reloading of your application.
The Vite configuration snippet you provided is a common way to address this issue. The usePolling: true
configuration tells Vite to use polling to detect file changes. Although this method could slightly increase CPU usage, it’s generally acceptable for most cases.
Setting interval: 500
configures Vite to check for file changes every 500 milliseconds.
Here is a complete vite.config.ts
file example:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
watch: {
usePolling: true,
interval: 500
}
},
})
Creating Google Cloud APIs & Service Credentials
To utilize Google OAuth2 for authentication, the following steps are required and should be performed in Google Cloud Console:
- Enable
Google+ API
: Search forAPIs & Service
, click onLibrary
on the left side, find Google+ API, and set its status to Enable. - Select Project: Create a new Project or choose an existing one.
- Create OAuth 2.0 Client IDs: Click on
Credentials
on the left side, and create aWeb Application
through+ CREATE CREDENTIALS
to obtain Client IDs. - Configure the OAuth consent screen: This is the authorization page that pops up when a user logs in with their Google Account. It can be configured here.
For a detailed setup process, you can refer to this YouTube tutorial: How to generate Google OAuth Client ID and Client Secret?
OAuth2 Login Authentication Process
OAuth 2.0 offers four major authorization flows, applicable to different types of applications:
-
Authorization Code Grant: This is the most common and secure authorization flow, suitable for a separated front-end and back-end design.
-
Implicit Grant: It directly returns an access token to obtain user information. Due to security reasons, this flow has been deprecated in OAuth 2.1.
-
Resource Owner Password Credentials Grant: The username and password are sent to the authorization server to obtain an access token. This flow is only suitable for server-to-server information exchange and has also been deprecated in OAuth 2.1.
-
Client Credentials Grant: In this flow, the application uses its client ID and secret to directly obtain an access token from the authorization server. This flow is suitable for cases where the application needs to acquire an access token on its behalf (not on behalf of the user).
Below is the UML Sequence Diagram for Google OAuth 2 utilizing Implicit Grant, which is also the method adopted by this project:
The following UML Sequence Diagram is for OAuth 2.0 Authorization Code Grant. The main difference from the implicit grant is that the user obtains an authorization code (instead of an access token), and the backend uses this authorization code to obtain the access token. Therefore, the access token is only retained in the backend and will not leak to the frontend, thereby enhancing security.
Using the yobaji/vue3-google-login package
The yobaji/vue3-google-login
is a lightweight plugin leveraging Google Identity Services and Google’s 3rd Party Authorization JavaScript Library to simplify Google login and signup workflows. This package utilizes the OAuth 2.0 Implicit Grant flow to secure user authentication. It offers the following functionalities:
- Login with a Google button
- Login using a One Tap prompt
- Automatic login without any user interaction
- Login with Google using a custom button
The first step is to install it:
yarn add vue3-google-login
# or
npm install vue3-google-login
Then, initialize the plugin:
import { createApp } from 'vue'
import App from './App.vue'
import vue3GoogleLogin from 'vue3-google-login'
const app = createApp(App)
app.use(vue3GoogleLogin, {
clientId: 'YOUR_GOOGLE_CLIENT_ID'
})
app.mount('#app')
ID Token
The following code will pop up the authentication window provided by Google. Once authorization is successful, the ID Token can be obtained from the returned credential field:
<script setup>
const callback = (response) => {
// This callback will be triggered when the user selects or login to
// his Google account from the popup
console.log("Handle the response", response)
}
</script>
<template>
<GoogleLogin :callback="callback"/>
</template>
The returned content is as follows:
clientId: '657755...',
client_id: '657755...', ,
credential: 'eyJhbGciOiJ...',
select_by: 'btn'
The credential is in JWT format, and the decoded information is as follows:
"iss": "https://accounts.google.com",
"nbf": 16854...,
"aud": "657755...",
"sub": "104222...",
"email": "dyson...",
"email_verified": true,
"azp": "6577558...",
"name": "Liu Dyson..",
"picture": "https://lh3.goog...",
"given_name": "Liu",
"family_name": "Dyson",
"iat": 16854...,
"exp": 16855...,
"jti": "07e4fc..."
Through the ID Token, the frontend can directly obtain the user’s profile. Although the data can be transmitted from the frontend to the backend, the backend cannot verify its authenticity. The frontend can switch to using the Access Token method, which is transmitted to the backend via the API. The backend retrieves userinfo from the Google Identity Service, which ensures the backend receives authentic information.
However, keep in mind, regardless of whether you adopt the ID Token or Access Token method, the frontend exposes important user information. To meet higher security requirements, the Authorization Code Grant process should be used.
Access Token
If you want to obtain an Access Token, you need to set popup-type to “TOKEN” and use a Custom Login Button. Afterward, send the Access Token to the backend so that the backend can verify its identity. Here is an example:
<script setup>
const callback = (response) => {
console.log("Handle the response", response)
}
</script>
<template>
<GoogleLogin :callback="callback" popup-type="TOKEN">
<v-btn prepend-icon="mdi-google" stacked>
Log in with Google
</v-btn>
</GoogleLogin>
</template>
Another way to acquire an access token is through the googleTokenLogin function. Here’s a sample:
<script setup>
import { googleTokenLogin } from "vue3-google-login"
const login = () => {
googleTokenLogin().then((response) => {
console.log("Handle the response", response)
})
}
</script>
<template>
<button @click="login">Login Using Google</button>
</template>
The returned response in json format is as follows:
{
"access_token": "ya29.a0AWY7CkmK6...",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "email profile openid https://www.googleapis.com/...",
"authuser": "0",
"prompt": "none"
}
Authorization Code
Using the Authorization Code method to avoid frontend leaking access tokens is a safer approach. However, the process is relatively complex and requires backend assistance to complete. The following example uses the googleAuthCodeLogin function to obtain an authorization code. This is only part of the code. In practice, the authorization code needs to be sent to the backend, and the backend needs to use the client secret, redirect URI, and other information to obtain the access token.
<script setup>
import { googleAuthCodeLogin } from "vue3-google-login"
const login = () => {
googleAuthCodeLogin().then((response) => {
console.log("Handle the response", response)
// Send the code from the response to the backend via an API, and let the backend retrieve the access token
})
}
</script>
<template>
<button @click="login">Login Using Google</button>
</template>
The content of the Authorization Code generally looks like this:
{
"code": "4/0AbUR2VN7RD...",
"scope": "email profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid",
"authuser": "0",
"prompt": "consent"
}
Calling backend API to log in
After successfully obtaining the access token, you can log in through the backend API, allowing the backend to obtain user-related information with the access token.
Below is a code example. Subsequently, we will develop the backend API specified by backendUrl to execute the login operation and return the login result and userinfo, etc.
<script setup>
const callback = async (response) => {
console.log("Handle the response", response)
const backendUrl = `http://${window.location.hostname}:5000/api/AuthGoogle/TokenLogin`
const res = await fetch(backendUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(response)
})
const data = await res.json()
console.log('Server response:', data)
}
</script>
Backend
Setup Kestrel configuration
To facilitate testing on localhost, we specify the listening port through builder.WebHost.ConfigureKestrel(), so that the frontend can call the backend API with a fixed URL.
The setup is as follows:
var builder = WebApplication.CreateBuilder(args);
// ... other code
// Setup Kestrel configuration
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000); // HTTP port
options.ListenAnyIP(5001, listenOptions => // HTTPS port
{
listenOptions.UseHttps();
});
});
// ... other code
var app = builder.Build();
Add CORS policy
Since the frontend runs in VS Code Dev Container, and the backend is developed and run on the local machine with Visual Studio, even though the domain name is localhost, the ports are different. Therefore, a CORS policy needs to be set up.
In ASP.NET Core 6.0, setting up a CORS policy involves two steps: first, add a service with builder.Services.AddCors() in Program.cs, then add middleware with app.UseCors(). The modified Program.cs example is as follows:
var allowSpecificOrigins = "_allowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add CORS policy
builder.Services.AddCors(options =>
{
options.AddPolicy(
name: allowSpecificOrigins,
policy =>
{
policy.WithOrigins("http://localhost:3000", "http://127.0.0.1:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// Setup Kestrel configuration
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(5000); // HTTP port
options.ListenAnyIP(5001, listenOptions => // HTTPS port
{
listenOptions.UseHttps();
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors(allowSpecificOrigins);
app.UseAuthorization();
app.MapControllers();
app.Run();
Add login api
Add a class AuthGoogleController : Controller
under the Controllers directory, and provide the HttpPost login API.
The example is as follows:
using Microsoft.AspNetCore.Mvc;
namespace oauth_google.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthGoogleController : Controller
{
[HttpPost("TokenLogin")]
public async Task<ActionResult<LoginResponse>> TokenLogin([FromBody] GoogleLoginRequest googleLoginRequest)
{
LoginResponse funcResponse = new();
return Ok(funcResponse);
}
}
}
In the above code, we have designed class GoogleLoginRequest and LoginResponse for HTTP request and response, respectively. The codes are as follows.
GoogleLoginRequest:
using System.Text.Json.Serialization;
namespace oauth_google.Models
{
public class GoogleLoginRequest
{
[JsonPropertyName("access_token")]
public string? AccessToken { get; set; }
}
}
LoginResponse:
namespace oauth_google.Models
{
public class LoginResponse
{
public bool Result { get; set; } = false;
public string Message { get; set; } = string.Empty;
public UserProfile? UserProfile { get; set; }
}
}
Call Google APIs to obtain userinfo
Access Token
Once a valid access token is received from the client, the backend server can call https://www.googleapis.com/oauth2/v3/userinfo?access_token
={accessToken} to obtain userinfo. Hence, registration and login operations can be completed at the same time. The example code is as follows:
using Microsoft.AspNetCore.Mvc;
namespace oauth_google.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthGoogleController : Controller
{
[HttpPost("TokenLogin")]
public async Task<ActionResult<LoginResponse>> TokenLogin([FromBody] GoogleLoginRequest googleLoginRequest)
{
LoginResponse funcResponse = new();
try
{
string? accessToken = googleLoginRequest.AccessToken ?? throw new Exception();
using HttpClient httpClient = new();
var response = await httpClient.GetAsync($"https://www.googleapis.com/oauth2/v3/userinfo?access_token={accessToken}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
// Get userinfo from Google API response
GoogleUserInfo? googleUserProfile = System.Text.Json.JsonSerializer.Deserialize<GoogleUserInfo>(content);
return Ok(funcResponse);
}
funcResponse.Result = false;
funcResponse.Message = "Invalid token.";
return BadRequest(funcResponse);
}
catch (Exception)
{
funcResponse.Result = false;
funcResponse.Message = "Invalid or expired token.";
return Unauthorized();
}
}
}
}
In the above example, the content of the class GoogleUserInfo is as follows:
using System.Text.Json.Serialization;
namespace oauth_google.Models
{
public class GoogleUserInfo
{
[JsonPropertyName("sub")]
public string? Sub { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("given_name")]
public string? GivenName { get; set; }
[JsonPropertyName("family_name")]
public string? FamilyName { get; set; }
[JsonPropertyName("picture")]
public string? Picture { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("email_verified")]
public bool? EmailVerified { get; set; }
[JsonPropertyName("locale")]
public string? Locale { get; set; }
}
}
Authorization Code
To retrieve userinfo using an authorization code involves a two-step process. Initially, the authorization code is used to obtain the access token from https://oauth2.googleapis.com/token. Afterward, userinfo can be accessed. The following code snippet showcases this, illustrating how the backend obtains the access token using the authorization code, client id, client secret, and redirect uri.
private static readonly string GoogleApiUrl = "https://www.googleapis.com/oauth2/v3/userinfo";
private static readonly string GoogleTokenUrl = "https://oauth2.googleapis.com/token";
[HttpPost("AuthCodeLogin")]
public async Task<ActionResult<ResponseBase<UserProfile>>> AuthCodeLogin([FromBody] GoogleAuthCodeLoginRequest googleLoginRequest)
{
ResponseBase<UserProfile> funcResponse = new();
try
{
// check request format
string? authCode = googleLoginRequest.Code ?? throw new Exception();
// Exchange authorization code for access token
var tokenRequestBody = new Dictionary<string, string>()
{
{"code", authCode},
{"client_id", _configuration["Google:ClientId"]},
{"client_secret", _configuration["Google:ClientSecret"]},
{"redirect_uri", _configuration["Google:RedirectUri"]},
{"grant_type", "authorization_code"}
};
using HttpClient httpClient = new();
var tokenResponse = await httpClient.PostAsync(GoogleTokenUrl, new FormUrlEncodedContent(tokenRequestBody));
if (!tokenResponse.IsSuccessStatusCode)
{
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
_logger.LogError("Failed to exchange authorization code for access token: {StatusCode}. Response content: {ResponseContent}", tokenResponse.StatusCode, errorContent);
return Unauthorized();
}
var tokenContent = await tokenResponse.Content.ReadAsStringAsync();
var tokenInfo = JsonSerializer.Deserialize<GoogleTokenInfo>(tokenContent);
if (tokenInfo == null || tokenInfo.AccessToken == null)
{
_logger.LogError(message: "Failed to deserialize exchange authorization code for access token");
return Unauthorized();
}
// Use access token to get user info
var request = new HttpRequestMessage(HttpMethod.Get, GoogleApiUrl);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenInfo.AccessToken);
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
GoogleUserInfo? googleUserProfile = System.Text.Json.JsonSerializer.Deserialize<GoogleUserInfo>(content);
if (googleUserProfile != null && !string.IsNullOrWhiteSpace(googleUserProfile.Sub))
{
UserProfile? userProfile = SaveUserProfile(googleUserProfile);
funcResponse.Result = true;
funcResponse.Data = userProfile;
return Ok(funcResponse);
}
}
funcResponse.Message = "Invalid authorization code.";
return BadRequest(funcResponse);
}
catch (Exception)
{
funcResponse.Message = "Invalid Authorization Code.";
return Unauthorized();
}
}
Vite Proxy
When testing the Frontend initiated with yarn dev
, the built-in web server (Koa) from Vite is utilized, while the Backend operates on Kestrel. Consequently, different ports need to be adopted for delineation. For instance, the Frontend can use http://localhost:3000, while the Backend employs http://localhost:5000. Given the different ports, the Backend needs to configure a CORS (Cross-Origin Resource Sharing) policy to smoothly permit connections from the Frontend.
Vite offers a Proxy feature, leveraging this mechanism can obviate the necessity of setting a CORS policy on the Backend during local testing of Frontend and Backend. The method entails adding a proxy configuration in the server section of the vite.config.ts
, as shown in the example below:
server: {
port: 3000,
watch: {
usePolling: true,
interval: 500
},
proxy: {
'/api': 'http://localhost:5000'
}
}
However, it’s crucial to note that Vite’s Proxy mechanism becomes ineffective when the Frontend is operating in a Dev Container. It can only function correctly in a local Node.js development environment.
Establishing a Testing Environment with Containers
We’ll be carrying out integration tests using containers. The Frontend uses Nginx as its Web Server, while the Backend, because of .NET Core’s built-in Kestrel, can run directly within a container. Additionally, we’re introducing an Nginx container responsible for Reverse Proxy, determining whether to direct to Frontend or Backend based on whether the first-level subdirectory path contains /api.
The architecture diagram is represented with a Flowchart below:
Creating the Frontend Docker Image
In the following example, we employ a multistage build to compile a program developed using Vue 3 in a pristine environment. The resulting files are subsequently copied and executed within Nginx.
FROM node:18 AS build
WORKDIR /src
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn run build
FROM nginx:alpine AS final
WORKDIR /app
COPY --from=build /src/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
The above example employs Nginx as the Web Server, so in addition to the Frontend code, an Nginx configuration file is also required. Here is an example of the configuration file:
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
The following example shows how to use docker build
to create a Docker image according to the contents of the Dockerfile. This example is suitable for Windows PowerShell.
docker build -t vue-dotnet-oauth-example/oauth-frontend-app:1.0 .
Creating the Backend Docker Image
The Backend is developed using .NET Core, which automatically generates a Dockerfile when the project is created. The method for creating the Docker Image is also multistage, like the Frontend. However, it is important to note that the Docker context generated automatically by Visual Studio should be set at the parent directory, i.e., the solution directory, rather than the project directory.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["oauth-google/oauth-google.csproj", "oauth-google/"]
RUN dotnet restore "oauth-google/oauth-google.csproj"
COPY . .
WORKDIR "/src/oauth-google"
RUN dotnet build "oauth-google.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "oauth-google.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "oauth-google.dll"]
The following example shows how to use the docker build
command to create a Docker image from the Dockerfile. The Dockerfile and the Project are in the same directory, but the context must be set to the parent directory.
docker build -t vue-dotnet-oauth-example/oauth-backend-api:1.0 -f Dockerfile ..
Integration Testing
Once the Dockerfiles for both the Frontend and Backend are complete, we can use docker compose to set up the entire environment. Here is an example of a docker-compose.yml
file:
version: '3.8'
services:
oauth-frontend:
image: vue-dotnet-oauth-example/oauth-frontend-app:1.0
build:
context: ./oauth-frontend/oauth-app
dockerfile: Dockerfile
networks:
- oauth-internal
oauth-backend:
image: vue-dotnet-oauth-example/oauth-backend-api:1.0
build:
context: ./oauth-backend
dockerfile: oauth-google/Dockerfile
environment:
- Google__ClientId=${GOOGLE_CLIENT_ID}
- Google__ClientSecret=${GOOGLE_CLIENT_SECRET}
- Google__RedirectUri=${GOOGLE_REDIRECT_URI}
- AllowedOrigins=http://localhost:3000,http://127.0.0.1:3000
networks:
- oauth-internal
nginx:
image: vue-dotnet-oauth-example/oauth-proxy:1.0
build:
context: .
dockerfile: Dockerfile.nginx
ports:
- 80:80
- 443:443
depends_on:
- oauth-backend
- oauth-frontend
networks:
- oauth-internal
networks:
oauth-internal:
In the example above, Nginx serves as the reverse proxy with the following configuration:
events {}
http {
server {
listen 80;
location /api {
proxy_pass http://oauth-backend:80;
}
location / {
proxy_pass http://oauth-frontend:80;
}
}
}
The directory structure for the docker-compose.yml
, Frontend, Backend and related file locations is as shown below:
vue-dotnet-oauth-example
├── oauth-backend
│ └── oauth-google
│ └─ Dockerfile
├── oauth-frontend
│ └── oauth-app
│ ├─ Dockerfile
│ └─ nginx.conf
├── docker-compose.yml
└── nginx.conf
Docker Compose Up/Down
The following command can be used to start Docker Compose, setting the relevant details for Google APIs & Services into the environment variables (for PowerShell):
$Env:GOOGLE_CLIENT_ID="your-client-id"
$Env:GOOGLE_CLIENT_SECRET="your-client-secret"
$Env:GOOGLE_REDIRECT_URI="your-redirect-uri"
$Env:ALLOWED_ORIGINS="your-allowed-origins"
docker-compose up --build
--build
: This optional flag instructs Docker to build images prior to launching the containers. If any modifications have been made to the Dockerfiles since the last time the containers were initiated or the images were built, those modifications will be included when the containers start.
To stop Docker Compose, you can use the following command (for PowerShell):
docker-compose down
Remove-Item Env:\GOOGLE_CLIENT_ID
Remove-Item Env:\GOOGLE_CLIENT_SECRET
Remove-Item Env:\GOOGLE_REDIRECT_URI
Remove-Item Env:\ALLOWED_ORIGINS
Pushing Images to Google Artifact Registry
We have successfully developed and tested three Docker images using docker-compose. Now, we are ready to deploy these on Google Cloud.
Google Cloud Platform’s (GCP) Cloud Run is a convenient service, ideal for quickly deploying and showcasing applications. It offers a serverless environment, manages the infrastructure, and automatically handles network and HTTPS support. This reduces the need to set up a reverse proxy server. In our example, we will deploy the frontend and backend on two separate Cloud Run instances. However, Firebase Hosting is another option for deploying static websites, such as frontend applications. Cloud Run is free within certain limits, making it an excellent choice for demonstrations.
Before we deploy, we need to push our Docker Images, currently stored locally, to the Google Artifact Registry. Suppose we have already created a project via the Google Cloud Console named “vue-dotnet-oauth”. The following steps outline the process:
- Under the “vue-dotnet-oauth” project, search for “Artifact Registry”. Click to enter, then click the “Enable” button to activate the Artifact Registry.
- Press “+” on the top toolbar and create a repository. Here’s a reference for setting up the various fields:
- Name: vue-dotnet-example-repos
- Format: [Docker] / Maven / npm / Python / Apt / Yum / Kubeflow Pipeline / Go
- Mode: [Standard] / Remote / Virtual
- Location type: [Region] / Multi-region
- Region: [asia-east1(Taiwan)] (You can choose the best data center location based on your area)
- Description: vue-dotnet-example Docker Repository
- Encryption: [Google-managed encryption key] / Customer-managed encryption key (CMEK)
- Press [Create]
- First, use
gcloud auth login
in PowerShell to log in. If you haven’t installed the gcloud CLI yet, please refer to the Install the gcloud CLI official website for installation steps. - Then, enter the following command to authorize the Docker repository. The command is
gcloud auth configure-docker <hostname>
. Here<hostname>
is region + “-docker.pkg.dev”. For example, for “asia-east1”, the command is:
You can replacegcloud auth configure-docker asia-east1-docker.pkg.dev
asia-east1
with the region where you created your repository. - Use the following command to check your local Docker configuration:
The displayed result should look something like this:cat $env:UserProfile\.docker\config.json
{ "credsStore": "desktop", "credHelpers": { "asia-east1-docker.pkg.dev": "gcloud" } }
- Before pushing, you need to tag the Docker Images. The rule is
docker tag <imagename> <hostname>/<project-name>/<repository-name>/<image-name>:<tag>
. Here are command examples:docker tag vue-dotnet-oauth-example/oauth-frontend-app:1.0 asia-east1-docker.pkg.dev/vue-dotnet-oauth/vue-dotnet-ex-docker-repository/oauth-frontend-app:1.0 docker tag vue-dotnet-oauth-example/oauth-backend-api:1.0 asia-east1-docker.pkg.dev/vue-dotnet-oauth/vue-dotnet-ex-docker-repository/oauth-backend-api:1.0
- Finally, push the tagged Docker images to the Docker repository with the following commands:
docker push asia-east1-docker.pkg.dev/vue-dotnet-oauth/vue-dotnet-ex-docker-repository/oauth-frontend-app:1.0 docker push asia-east1-docker.pkg.dev/vue-dotnet-oauth/vue-dotnet-ex-docker-repository/oauth-backend-api:1.0
Deployment to GCP Cloud
Up until now, we have successfully pushed our Docker images to the Artifact Registry. However, to activate the programs on Cloud Run, we still need to make some adjustments, such as setting the listening port for the Frontend and the URI for connecting to the Backend API. In addition, there are some preliminary tasks to enable Cloud Run, which need to be executed in the Google Cloud Console.
Create Cloud Run Service
Firstly, we must establish Cloud Run in order to obtain a public domain name, so that we can set the URI for the Backend API in the Frontend. The steps are as follows:
Create Backend Cloud Run Service
- Search for
Cloud Run
in the Google Cloud Console and selectCREATE SERVICE
. - Choose the Backend image that was uploaded earlier and specify the Region.
- Expand the
Container, Networking, Security
section, and input the following information in the Environment Variables:- ASPNETCORE_URLS: http://+:8080 (By default, the Cloud sets the internal listening port of the container to 8080. If you want to modify this, you’ll need to adjust the
Container port
setting at the same time) - AllowedOrigins:
<empty>
(Leave it blank for now, fill it in after creating the Frontend Cloud Run) - Google__ClientId:
<empty>
(Leave it blank for now, fill it in after creating OAuth) - Google__ClientSecret:
<empty>
(Leave it blank for now, fill it in after creating OAuth) - Google__RedirectUri:
<empty>
(Leave it blank for now, fill it in after creating the Frontend Cloud Run. Please note that GCP has an implicit rule. When exchanging an Authorization Code for an Access Token, the Backend calls the Google API. The RedirectUri cannot be the URL of the caller. Thus, it’s recommended to fill in the URI of the Frontend later) - After completing these steps, click
DEPLOY
. If created successfully, you will obtain the URI for the Backend’s external service.
- ASPNETCORE_URLS: http://+:8080 (By default, the Cloud sets the internal listening port of the container to 8080. If you want to modify this, you’ll need to adjust the
Create Frontend Cloud Run Service
The Frontend, as a static webpage, is unable to dynamically read environment variables to specify the location of the Backend API connection. Thus, we need to modify the build method. The Frontend and Backend are on two independent Cloud Runs, and connection is achieved via a public domain name. We add a build:cloud_run script in the package.json and add .env and .env.cloud in the root directory to set the Backend URI. Here are examples of the relevant file modifications:
package.json:
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build:cloud_run": "vue-tsc --noEmit && vite build --mode cloud_run",
"preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore"
},
.env
VITE_BACKEND_URI=
.env.cloud_run
VITE_BACKEND_URI=<backend base uri>
Modify the VUE 3 program that calls the Backend API
// const backendUrl = '/api/AuthGoogle/ClientInfo'
// Add the environment variable setting for the Backend API Base URI to the above program
const backendUrl = `${import.meta.env.VITE_BACKEND_URI || ''}/api/AuthGoogle/ClientInfo`
Since Cloud Run uses the environment variable PORT to set the container’s internal listening port, nginx.conf also needs some adjustments. Set the listening configuration to ${PORT}.
server {
listen ${PORT};
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Finally, rebuild the Docker Image through docker build and upload it to the Artifact Registry.
# docker build
docker build --build-arg BUILD_MODE=cloud_run -t asia-east1-docker.pkg.dev/vue-dotnet-oauth/vue-dotnet-ex-docker-repository/oauth-frontend-app:1.0 .
# docker push
docker push asia-east1-docker.pkg.dev/vue-dotnet-oauth/vue-dotnet-ex-docker-repository/oauth-frontend-app:1.0
After updating the Docker image, you can create a Cloud Run Service in the same way as the Backend. Cloud Run will automatically set the environment variable PORT to specify the container’s internal listening port, and we have already set the corresponding variable in nginx.conf, so there’s no need for additional environment variable settings in the Frontend Cloud Run.
Establish Google OAuth 2.0 API
Our example mainly demonstrates logging in through OAuth 2.0 using a Google account. Therefore, we need to set up OAuth 2.0 related information as follows:
- Search for
APIs & Service
in the Google Cloud Console. After entering, click on+ ENABLE APIS AND SERVICES
, look forGoogle+ API
, and pressENABLE
. Enabling the Google+ API allows the program we developed to execute OAuth authentication operations through the API. - In the menu of
APIs & Service
, click onOAuth consent screen
and create asExternal
. Fill in the necessary fields. When users click login, the pop-up dialog screen is set up via the OAuth consent screen. - In the menu of
APIs & Service
, click onCredentials
. At the top of the screen, pressCREATE CREDENTIALS
and selectOAuth Client ID
. Enter the URI of the frontend and backend cloud run service inAuthorized JavaScript origins
, and enter the URI of the frontend cloud run service inAuthorized redirect URIs
. As mentioned earlier, GCP OAuth 2.0 has an implicit rule: if the redirect URI is the same as the sent request (API that exchanges the authorization code for an access code), the comparison of the redirect URI will fail.
Reset the Environmental Variables of the Backend Cloud Run
After completing the OAuth setup, you can set the Client ID, Client Secret, and Redirect URI into the Backend’s environmental variables: Google__ClientId, Google__ClientSecret, and Google__RedirectUri. At the same time, remember to assign the content of AllowedOrigins as the URI of the Frontend Cloud Run.
The entire process is generally complete, although it can be somewhat complicated. Practice a few times, and you should be able to complete it smoothly. Finally, remember that changes to the contents of Credentials are not immediately effective. It may take a few minutes to several hours, which can cause some uncertainty when determining whether the settings are effective.