Skip to content
This repository was archived by the owner on Jul 12, 2023. It is now read-only.

Commit bee3756

Browse files
authored
add well defined errorCode strings in responses (#216)
* add well defined errorCode strings in responses * put in default case * fix tag error
1 parent 279008a commit bee3756

File tree

4 files changed

+85
-28
lines changed

4 files changed

+85
-28
lines changed

pkg/api/api.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,36 @@ const (
2525
TestTypeLikely = "likely"
2626
// TestTypeNegative is the string that represents a negative test.
2727
TestTypeNegative = "negative"
28+
29+
// error_code definitions for the APIs.
30+
// General
31+
ErrUnparsableRequest = "unparsable_request"
32+
ErrInternal = "internal_server_error"
33+
34+
// Verify API responses
35+
// ErrVerifyCodeInvalid indicates the code entered is unknown or already used.
36+
ErrVerifyCodeInvalid = "code_invalid"
37+
// ErrVerifyCodeExpired indicates the code provided is known to the server, but expired.
38+
ErrVerifyCodeExpired = "code_expired"
39+
40+
// Certificate API responses
41+
// ErrTokenInvalid indicates the token provided is unknown or already used
42+
ErrTokenInvalid = "token_invalid"
43+
// ErrTokenExpired indicates that the token provided is known but expired.
44+
ErrTokenExpired = "token_expired"
45+
// ErrHMACInvalid indicates that the HMAC that is being signed is invalid (wrong length)
46+
ErrHMACInvalid = "hmac_invalid"
2847
)
2948

3049
// ErrorReturn defines the common error type.
3150
type ErrorReturn struct {
32-
Error string `json:"error"`
51+
Error string `json:"error"`
52+
ErrorCode string `json:"error_code"`
53+
}
54+
55+
// InternalError constructs a generic internal error.
56+
func InternalError() *ErrorReturn {
57+
return Errorf("internal error").WithCode(ErrInternal)
3358
}
3459

3560
// Errorf creates an ErrorReturn w/ the formateed message.
@@ -46,10 +71,17 @@ func Error(err error) *ErrorReturn {
4671
return &ErrorReturn{Error: err.Error()}
4772
}
4873

74+
// WithCode adds an error code to an ErrorReturn
75+
func (e *ErrorReturn) WithCode(code string) *ErrorReturn {
76+
e.ErrorCode = code
77+
return e
78+
}
79+
4980
// CSRFResponse is the return type when requesting an AJAX CSRF token.
5081
type CSRFResponse struct {
5182
CSRFToken string `json:"csrftoken"`
5283
Error string `json:"error"`
84+
ErrorCode string `json:"errorCode"`
5385
}
5486

5587
// IssueCodeRequest defines the parameters to request an new OTP (short term)
@@ -68,6 +100,7 @@ type IssueCodeResponse struct {
68100
ExpiresAt string `json:"expiresAt"` // RFC1123 string formatted timestamp, in UTC.
69101
ExpiresAtTimestamp int64 `json:"expiresAtTimestamp"` // Unix, seconds since the epoch. Still UTC.
70102
Error string `json:"error"`
103+
ErrorCode string `json:"errorCode,omitempty"`
71104
}
72105

73106
// VerifyCodeRequest is the request structure for exchanging a short term Verification Code
@@ -82,10 +115,11 @@ type VerifyCodeRequest struct {
82115
// (type and [optional] date) as well as the verification token. The verification token
83116
// may be sent back on a valid VerificationCertificateRequest later.
84117
type VerifyCodeResponse struct {
85-
TestType string `json:"testtype"`
86-
SymptomDate string `json:"symptomDate"` // ISO 8601 formatted date, YYYY-MM-DD
87-
VerificationToken string `json:"token"` // JWT - signed, not encrypted.
88-
Error string `json:"error"`
118+
TestType string `json:"testtype,omitempty"`
119+
SymptomDate string `json:"symptomDate,omitempty"` // ISO 8601 formatted date, YYYY-MM-DD
120+
VerificationToken string `json:"token,omitempty"` // JWT - signed, not encrypted.
121+
Error string `json:"error,omitempty"`
122+
ErrorCode string `json:"errorCode,omitempty"`
89123
}
90124

91125
// VerificationCertificateRequest is used to accept a long term token and
@@ -103,6 +137,7 @@ type VerificationCertificateRequest struct {
103137
// a signed certificate that can be presented to the configured exposure
104138
// notifications server to publish keys along w/ the certified diagnosis.
105139
type VerificationCertificateResponse struct {
106-
Certificate string `json:"certificate"`
107-
Error string `json:"error"`
140+
Certificate string `json:"certificate,omitempty"`
141+
Error string `json:"error,omitempty"`
142+
ErrorCode string `json:"errorCode,omitempty"`
108143
}

pkg/controller/certapi/certificate.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
package certapi
1616

1717
import (
18+
"errors"
1819
"net/http"
1920
"time"
2021

2122
"github.com/dgrijalva/jwt-go"
2223
"github.com/google/exposure-notifications-server/pkg/base64util"
2324
"github.com/google/exposure-notifications-verification-server/pkg/api"
2425
"github.com/google/exposure-notifications-verification-server/pkg/controller"
26+
"github.com/google/exposure-notifications-verification-server/pkg/database"
2527
"github.com/google/exposure-notifications-verification-server/pkg/jwthelper"
2628

2729
verifyapi "github.com/google/exposure-notifications-server/pkg/api/v1"
@@ -33,48 +35,50 @@ func (c *Controller) HandleCertificate() http.Handler {
3335

3436
authApp := controller.AuthorizedAppFromContext(ctx)
3537
if authApp == nil {
38+
c.logger.Errorf("missing authorized app")
3639
controller.MissingAuthorizedApp(w, r, c.h)
3740
return
3841
}
3942

4043
publicKey, err := c.getPublicKey(ctx, c.config.TokenSigningKey)
4144
if err != nil {
4245
c.logger.Errorw("failed to get public key", "error", err)
43-
c.h.RenderJSON(w, http.StatusInternalServerError, nil)
46+
c.h.RenderJSON(w, http.StatusInternalServerError, api.InternalError())
4447
return
4548
}
4649

4750
// Get the signer based on Key configuration.
4851
signer, err := c.signer.NewSigner(ctx, c.config.CertificateSigningKey)
4952
if err != nil {
5053
c.logger.Errorw("failed to get signer", "error", err)
51-
c.h.RenderJSON(w, http.StatusInternalServerError, nil)
54+
c.h.RenderJSON(w, http.StatusInternalServerError, api.InternalError())
5255
return
5356
}
5457

5558
var request api.VerificationCertificateRequest
5659
if err := controller.BindJSON(w, r, &request); err != nil {
57-
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
60+
c.logger.Errorf("failed to parse json request", "error", err)
61+
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrTokenInvalid))
5862
return
5963
}
6064

6165
// Parse and validate the verification token.
6266
tokenID, subject, err := c.validateToken(request.VerificationToken, publicKey)
6367
if err != nil {
64-
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
68+
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrTokenInvalid))
6569
return
6670
}
6771

6872
// Validate the HMAC length. SHA 256 HMAC must be 32 bytes in length.
6973
hmacBytes, err := base64util.DecodeString(request.ExposureKeyHMAC)
7074
if err != nil {
7175
c.h.RenderJSON(w, http.StatusBadRequest,
72-
api.Errorf("exposure key HMAC is not a valid base64: %v", err))
76+
api.Errorf("exposure key HMAC is not a valid base64: %v", err).WithCode(api.ErrHMACInvalid))
7377
return
7478
}
7579
if len(hmacBytes) != 32 {
7680
c.h.RenderJSON(w, http.StatusBadRequest,
77-
api.Errorf("exposure key HMAC is not the correct length, want: 32 got: %v", len(hmacBytes)))
81+
api.Errorf("exposure key HMAC is not the correct length, want: 32 got: %v", len(hmacBytes)).WithCode(api.ErrHMACInvalid))
7882
return
7983
}
8084

@@ -99,15 +103,24 @@ func (c *Controller) HandleCertificate() http.Handler {
99103
certificate, err := jwthelper.SignJWT(certToken, signer)
100104
if err != nil {
101105
c.logger.Errorw("failed to sign certificate", "error", err)
102-
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
106+
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrInternal))
103107
return
104108
}
105109

106110
// Do the transactional update to the database last so that if it fails, the
107111
// client can retry.
108112
if err := c.db.ClaimToken(authApp.RealmID, tokenID, subject); err != nil {
109113
c.logger.Errorw("failed to claim token", "tokenID", tokenID, "error", err)
110-
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
114+
switch {
115+
case errors.Is(err, database.ErrTokenExpired):
116+
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrTokenExpired))
117+
case errors.Is(err, database.ErrTokenUsed):
118+
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("verification token invalid").WithCode(api.ErrTokenExpired))
119+
case errors.Is(err, database.ErrTokenMetadataMismatch):
120+
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("verification token invalid").WithCode(api.ErrTokenExpired))
121+
default:
122+
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
123+
}
111124
return
112125
}
113126

pkg/controller/verifyapi/verify.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,16 @@ func (c *Controller) HandleVerify() http.Handler {
7474

7575
var request api.VerifyCodeRequest
7676
if err := controller.BindJSON(w, r, &request); err != nil {
77-
c.h.RenderJSON(w, http.StatusOK, api.Error(err))
77+
c.logger.Errorw("bad request", "error", err)
78+
c.h.RenderJSON(w, http.StatusOK, api.Error(err).WithCode(api.ErrUnparsableRequest))
7879
return
7980
}
8081

8182
// Get the signer based on Key configuration.
8283
signer, err := c.signer.NewSigner(ctx, c.config.TokenSigningKey)
8384
if err != nil {
8485
c.logger.Errorw("failed to get signer", "error", err)
85-
c.h.RenderJSON(w, http.StatusInternalServerError, nil)
86+
c.h.RenderJSON(w, http.StatusInternalServerError, api.InternalError())
8687
return
8788
}
8889

@@ -91,12 +92,16 @@ func (c *Controller) HandleVerify() http.Handler {
9192
verificationToken, err := c.db.VerifyCodeAndIssueToken(authApp.RealmID, request.VerificationCode, c.config.VerificationTokenDuration)
9293
if err != nil {
9394
c.logger.Errorw("failed to issue verification token", "error", err)
94-
if errors.Is(err, database.ErrVerificationCodeExpired) || errors.Is(err, database.ErrVerificationCodeUsed) {
95-
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
96-
return
95+
switch {
96+
case errors.Is(err, database.ErrVerificationCodeExpired):
97+
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("verification code expired").WithCode(api.ErrTokenExpired))
98+
case errors.Is(err, database.ErrVerificationCodeUsed):
99+
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("verification code invalid").WithCode(api.ErrTokenInvalid))
100+
case errors.Is(err, database.ErrVerificationCodeNotFound):
101+
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("verification code invalid").WithCode(api.ErrTokenInvalid))
102+
default:
103+
c.h.RenderJSON(w, http.StatusInternalServerError, api.InternalError())
97104
}
98-
99-
c.h.RenderJSON(w, http.StatusInternalServerError, nil)
100105
return
101106
}
102107

@@ -115,7 +120,7 @@ func (c *Controller) HandleVerify() http.Handler {
115120
signedJWT, err := jwthelper.SignJWT(token, signer)
116121
if err != nil {
117122
c.logger.Errorw("failed to sign token", "error", err)
118-
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
123+
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrInternal))
119124
return
120125
}
121126

pkg/database/token.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ const (
3131
)
3232

3333
var (
34-
ErrVerificationCodeExpired = errors.New("verification code expired")
35-
ErrVerificationCodeUsed = errors.New("verification code used")
36-
ErrTokenExpired = errors.New("verification token expired")
37-
ErrTokenUsed = errors.New("verification token used")
38-
ErrTokenMetadataMismatch = errors.New("verification token test metadata mismatch")
34+
ErrVerificationCodeNotFound = errors.New("verification code not found")
35+
ErrVerificationCodeExpired = errors.New("verification code expired")
36+
ErrVerificationCodeUsed = errors.New("verification code used")
37+
ErrTokenExpired = errors.New("verification token expired")
38+
ErrTokenUsed = errors.New("verification token used")
39+
ErrTokenMetadataMismatch = errors.New("verification token test metadata mismatch")
3940
)
4041

4142
// Token represents an issued "long term" from a validated verification code.
@@ -157,6 +158,9 @@ func (db *Database) VerifyCodeAndIssueToken(realmID uint, verCode string, expire
157158
// Also lock the row for update.
158159
var vc VerificationCode
159160
if err := db.db.Set("gorm:query_option", "FOR UPDATE").Where("code = ? and realm_id = ?", verCode, realmID).First(&vc).Error; err != nil {
161+
if gorm.IsRecordNotFoundError(err) {
162+
return ErrVerificationCodeNotFound
163+
}
160164
return err
161165
}
162166
if vc.IsExpired() {

0 commit comments

Comments
 (0)