Skip to content
This repository was archived by the owner on Jul 12, 2023. It is now read-only.
Merged
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
2 changes: 1 addition & 1 deletion assets/enx-redirect/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic-bootstrap.min.css"
integrity="sha256-BJ/G+e+y7bQdrYkS2RBTyNfBHpA9IuGaPmf9htub5MQ=" crossorigin="anonymous">

<title>Exposure Notifications Express Redirect Service</title>
<title>{{if .title}}{{.title}}{{else}}Exposure Notifications Express Redirect Service{{end}}</title>

<style type="text/css">
nav.navbar {
Expand Down
63 changes: 63 additions & 0 deletions assets/enx-redirect/report/header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{{define "report/header"}}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta data-build-id="{{.build_id}}" data-build-tag="{{.build_tag}}">

<link rel="shortcut icon" href="/static/favicon.ico">
<meta name="theme-color" content="#ffffff">
{{.csrfMeta}}

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic-bootstrap.min.css"
integrity="sha384-wWci3BOzr88l+HNsAtr3+e5bk9qh5KfjU6gl/rbzfTYdsAVHBEbxB33veLYmFg/a" crossorigin="anonymous">
<link rel="stylesheet"
href="/static/css/application.css?{{.buildID}}" crossorigin="anonymous" />

<script src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js"
integrity="sha384-+YQ4JLhjyBLPDQt//I+STsc9iw4uQqACwlvpslubQzn4u2UU2UFM80nGisd026JF" crossorigin="anonymous"></script>
<script src="/static/js/application.js?{{.buildID}}" crossorigin="anonymous"></script>

<title>{{if .title}}[{.title}}{{else}}Verification Code Request{{end}}</title>

<header class="mb-3">
<nav class="nav nav-tabs navbar-expand-md navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="#">Request Exposure Notifications verification code</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
</header>

{{end}}

{{define "errorable"}}
{{if .}}
<div class="invalid-feedback">
{{joinStrings . ", "}}
</div>
{{end}}
{{end}}

{{define "errorSummary"}}
{{if $errs := .Errors}}
<div class="alert alert-danger mb-4" role="alert">
<p>
The following errors occurred:
</p>

<ul class="mb-1">
{{range $k, $v := $errs}}
{{range $e := $v}}
<li><strong>{{$k}}</strong> {{$e}}</li>
{{end}}
{{end}}
</ul>
</div>
{{end}}
{{end}}
73 changes: 73 additions & 0 deletions assets/enx-redirect/report/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{{define "report/index"}}

{{$currentRealm := .realm}}

<!doctype html>
<html dir="{{$.textDirection}}" lang="{{$.textLanguage}}">
<head>
{{template "report/header" .}}
</head>

<body class="tab-content">
<main role="main" class="container">
<h1>Request Exposure Notifications verification code from {{.realm.Name}} ({{.realm.RegionCode}})</h1>

<form method="POST" action="/report/issue" class="floating-form">
{{ .csrfField }}

<div id="form-area">

<div class="card mb-3 shadow-sm">
<div class="card-header">Enter the date of your COVID-19 test</div>
<div class="card-body">
<div class="form-row">
<div class="form-group col-md-6">
<label for="testDate">{{t $.locale "codes.issue.testing-date-label"}}</label>
<input type="date" id="test-date" name="testDate" min="{{.minDate}}" max="{{.maxDate}}" class="form-control w-100" {{if $currentRealm.RequireDate}}required{{end}} />
</div>
</div>
</div>
</div>

<div class="card mb-3 shadow-sm">
<div class="card-header">Phone Number</div>
<div class="card-body">
<div class="form-row">
<label for="phone">Mobile phone number for receiving SMS
</label>
</div>
<div class="form-row">
<div class="input-group">
<input type="tel" id="phone" name="phone" class="form-control" autocomplete="off" class="d-block" />
</div>
</div>
</div>
</div>

<div class="card mb-3 shadow-sm">
<div class="card-header">Agreement</div>
<div class="card-body">
<div class="form-row">
<div class="form-check mb-3">
<input type="checkbox" name="agreement" id="agreement" class="form-check-input" value="true" />
<label for="agreement" class="form-check-label">
I agree that I have received a positive test result for COVID-19, that the phone
number provided belongs to me, and that I would like to receive a verification code
for Exposure Notifications to share my information anonymously with others.
</label>
</div>
</div>
</div>
</div>

<div class="row mb-3">
<div class="col">
<button id="submit" type="submit" class="btn btn-primary btn-block">{{t $.locale "codes.issue.create-code-button"}}</button>
</div>
</div>
</div>
</form>
</main>
</body>
</html>
{{end}}
19 changes: 19 additions & 0 deletions assets/enx-redirect/report/issue.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{define "report/issue"}}

{{$currentRealm := .realm}}

<!doctype html>
<html dir="{{$.textDirection}}" lang="{{$.textLanguage}}">
<head>
{{template "report/header" .}}
</head>

<body class="tab-content">
<main role="main" class="container">
<div class="alert alert-success" role="alert">Verification code issued</div>

<p>Please check your text messages on your mobile device.</p>
</main>
</body>
</html>
{{end}}
16 changes: 15 additions & 1 deletion cmd/enx-redirect/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import (
"github.com/google/exposure-notifications-verification-server/internal/routes"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/config"
"github.com/google/exposure-notifications-verification-server/pkg/ratelimit"
"github.com/gorilla/handlers"

"github.com/google/exposure-notifications-server/pkg/keys"
"github.com/google/exposure-notifications-server/pkg/logging"
"github.com/google/exposure-notifications-server/pkg/observability"
"github.com/google/exposure-notifications-server/pkg/server"
Expand Down Expand Up @@ -95,8 +97,20 @@ func realMain(ctx context.Context) error {
}
defer db.Close()

// Setup rate limiter
limiterStore, err := ratelimit.RateLimiterFor(ctx, &cfg.RateLimit)
if err != nil {
return fmt.Errorf("failed to create limiter: %w", err)
}
defer limiterStore.Close(ctx)

smsSigner, err := keys.KeyManagerFor(ctx, &cfg.SMSSigning.Keys)
if err != nil {
return fmt.Errorf("failed to create sms key manager: %w", err)
}

// Setup routes
mux, err := routes.ENXRedirect(ctx, cfg, db, cacher)
mux, err := routes.ENXRedirect(ctx, cfg, db, cacher, smsSigner, limiterStore)
if err != nil {
return fmt.Errorf("failed to setup routes: %w", err)
}
Expand Down
39 changes: 39 additions & 0 deletions docs/user-report-webview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!-- TOC depthFrom:1 -->

- [Experimental Notice](#experimental-notice)
- [Access](#access)
- [Initiate](#initiate)
- [Validate](#validate)

<!-- /TOC -->

# Experimental Notice

The user initiated report webview is an experimental feature. It should
not be deployed in production environments until this notice is removed.

# Access

To access the user report webview you need

* A `DEVICE` level API key
* The base URL for the EN Express redirect service

# Initiate

Open a webview with a POST request to the base URL for the EN Express
redirect service for your installation.

Pass in these required headers:

* `X-API-Key`: <your api key>
* `X-Nonce`: 256 bytes of random data, base64 encoded (base64 URL encoding recommended)

This will establish a session with the server and render a form for the user to fill out.

The verification code / link will be sent to the user's mobile phone number.

# Validate

The `nonce` that is generated when loading the webview should be passed
to the next call to `/api/verify` or forgotten after 24 hours.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/unrolled/secure v1.0.8
go.opencensus.io v0.23.0
go.uber.org/zap v1.16.0
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/text v0.3.6
golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a
Expand Down
10 changes: 10 additions & 0 deletions internal/clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ import (
// Option is a customization option for the client.
type Option func(c *client) *client

// WithCookieJar installs the specified cookier jar in the client. This
// is necessary if you are making successive requests that need to utilize
// cookies.
func WithCookieJar(jar http.CookieJar) Option {
return func(c *client) *client {
c.httpClient.Jar = jar
return c
}
}

// WithTimeout sets a custom timeout for each request. The default is 5s.
func WithTimeout(d time.Duration) Option {
return func(c *client) *client {
Expand Down
91 changes: 91 additions & 0 deletions internal/clients/enx_web.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package clients

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
)

// ENXRedirectWebClient is a client that talks to the enx-redirect web components (user-report).
type ENXRedirectWebClient struct {
*client
}

// NewENXRedirectWebClient creates a new enx-redirect service http client for user-report.
func NewENXRedirectWebClient(base string, apiKey string, opts ...Option) (*ENXRedirectWebClient, error) {
client, err := newClient(base, apiKey, opts...)
if err != nil {
return nil, err
}

return &ENXRedirectWebClient{
client: client,
}, nil
}

// SendUserReportIndex request "/report" on the ENX Redirect server which is the landing page
// for a client embedded webview. This requires a client with an installed cookiejar to work correctly
// since this will create a session cookie that embeds the nonce provided in the header.
func (c *ENXRedirectWebClient) SendUserReportIndex(ctx context.Context, nonce string) error {
req, err := c.newRequest(ctx, http.MethodPost, "/report", nil)
if err != nil {
return err
}
req.Header.Set("X-Nonce", nonce)

res, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("error making initial load request: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code from load request: %v", res.StatusCode)
}
return nil
}

// SendUserReportIssue issues a user-report verification code by posting the web form. Must be called from the same
// client
func (c *ENXRedirectWebClient) SendUserReportIssue(ctx context.Context, testDate string, phone string, agree string) error {
// Send the issue code request
values := &url.Values{
"testDate": []string{testDate},
"phone": []string{phone},
"agreement": []string{agree},
}

u := c.baseURL.ResolveReference(&url.URL{Path: "report/issue"})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(values.Encode()))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

res, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("error posting report form: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code from load request: %v", res.StatusCode)
}
return nil
}
Loading