code.gitea.io/gitea@v1.22.3/routers/web/auth/webauthn.go (about)

     1  // Copyright 2018 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package auth
     5  
     6  import (
     7  	"errors"
     8  	"net/http"
     9  
    10  	"code.gitea.io/gitea/models/auth"
    11  	user_model "code.gitea.io/gitea/models/user"
    12  	wa "code.gitea.io/gitea/modules/auth/webauthn"
    13  	"code.gitea.io/gitea/modules/base"
    14  	"code.gitea.io/gitea/modules/log"
    15  	"code.gitea.io/gitea/modules/setting"
    16  	"code.gitea.io/gitea/services/context"
    17  	"code.gitea.io/gitea/services/externalaccount"
    18  
    19  	"github.com/go-webauthn/webauthn/protocol"
    20  	"github.com/go-webauthn/webauthn/webauthn"
    21  )
    22  
    23  var tplWebAuthn base.TplName = "user/auth/webauthn"
    24  
    25  // WebAuthn shows the WebAuthn login page
    26  func WebAuthn(ctx *context.Context) {
    27  	ctx.Data["Title"] = ctx.Tr("twofa")
    28  
    29  	if CheckAutoLogin(ctx) {
    30  		return
    31  	}
    32  
    33  	// Ensure user is in a 2FA session.
    34  	if ctx.Session.Get("twofaUid") == nil {
    35  		ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
    36  		return
    37  	}
    38  
    39  	hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Session.Get("twofaUid").(int64))
    40  	if err != nil {
    41  		ctx.ServerError("HasTwoFactorByUID", err)
    42  		return
    43  	}
    44  
    45  	ctx.Data["HasTwoFactor"] = hasTwoFactor
    46  
    47  	ctx.HTML(http.StatusOK, tplWebAuthn)
    48  }
    49  
    50  // WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
    51  func WebAuthnLoginAssertion(ctx *context.Context) {
    52  	// Ensure user is in a WebAuthn session.
    53  	idSess, ok := ctx.Session.Get("twofaUid").(int64)
    54  	if !ok || idSess == 0 {
    55  		ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
    56  		return
    57  	}
    58  
    59  	user, err := user_model.GetUserByID(ctx, idSess)
    60  	if err != nil {
    61  		ctx.ServerError("UserSignIn", err)
    62  		return
    63  	}
    64  
    65  	exists, err := auth.ExistsWebAuthnCredentialsForUID(ctx, user.ID)
    66  	if err != nil {
    67  		ctx.ServerError("UserSignIn", err)
    68  		return
    69  	}
    70  	if !exists {
    71  		ctx.ServerError("UserSignIn", errors.New("no device registered"))
    72  		return
    73  	}
    74  
    75  	assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user))
    76  	if err != nil {
    77  		ctx.ServerError("webauthn.BeginLogin", err)
    78  		return
    79  	}
    80  
    81  	if err := ctx.Session.Set("webauthnAssertion", sessionData); err != nil {
    82  		ctx.ServerError("Session.Set", err)
    83  		return
    84  	}
    85  	ctx.JSON(http.StatusOK, assertion)
    86  }
    87  
    88  // WebAuthnLoginAssertionPost validates the signature and logs the user in
    89  func WebAuthnLoginAssertionPost(ctx *context.Context) {
    90  	idSess, ok := ctx.Session.Get("twofaUid").(int64)
    91  	sessionData, okData := ctx.Session.Get("webauthnAssertion").(*webauthn.SessionData)
    92  	if !ok || !okData || sessionData == nil || idSess == 0 {
    93  		ctx.ServerError("UserSignIn", errors.New("not in WebAuthn session"))
    94  		return
    95  	}
    96  	defer func() {
    97  		_ = ctx.Session.Delete("webauthnAssertion")
    98  	}()
    99  
   100  	// Load the user from the db
   101  	user, err := user_model.GetUserByID(ctx, idSess)
   102  	if err != nil {
   103  		ctx.ServerError("UserSignIn", err)
   104  		return
   105  	}
   106  
   107  	log.Trace("Finishing webauthn authentication with user: %s", user.Name)
   108  
   109  	// Now we do the equivalent of webauthn.FinishLogin using a combination of our session data
   110  	// (from webauthnAssertion) and verify the provided request.0
   111  	parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
   112  	if err != nil {
   113  		// Failed authentication attempt.
   114  		log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
   115  		ctx.Status(http.StatusForbidden)
   116  		return
   117  	}
   118  
   119  	// Validate the parsed response.
   120  	cred, err := wa.WebAuthn.ValidateLogin((*wa.User)(user), *sessionData, parsedResponse)
   121  	if err != nil {
   122  		// Failed authentication attempt.
   123  		log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
   124  		ctx.Status(http.StatusForbidden)
   125  		return
   126  	}
   127  
   128  	// Ensure that the credential wasn't cloned by checking if CloneWarning is set.
   129  	// (This is set if the sign counter is less than the one we have stored.)
   130  	if cred.Authenticator.CloneWarning {
   131  		log.Info("Failed authentication attempt for %s from %s: cloned credential", user.Name, ctx.RemoteAddr())
   132  		ctx.Status(http.StatusForbidden)
   133  		return
   134  	}
   135  
   136  	// Success! Get the credential and update the sign count with the new value we received.
   137  	dbCred, err := auth.GetWebAuthnCredentialByCredID(ctx, user.ID, cred.ID)
   138  	if err != nil {
   139  		ctx.ServerError("GetWebAuthnCredentialByCredID", err)
   140  		return
   141  	}
   142  
   143  	dbCred.SignCount = cred.Authenticator.SignCount
   144  	if err := dbCred.UpdateSignCount(ctx); err != nil {
   145  		ctx.ServerError("UpdateSignCount", err)
   146  		return
   147  	}
   148  
   149  	// Now handle account linking if that's requested
   150  	if ctx.Session.Get("linkAccount") != nil {
   151  		if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil {
   152  			ctx.ServerError("LinkAccountFromStore", err)
   153  			return
   154  		}
   155  	}
   156  
   157  	remember := ctx.Session.Get("twofaRemember").(bool)
   158  	redirect := handleSignInFull(ctx, user, remember, false)
   159  	if redirect == "" {
   160  		redirect = setting.AppSubURL + "/"
   161  	}
   162  	_ = ctx.Session.Delete("twofaUid")
   163  
   164  	ctx.JSONRedirect(redirect)
   165  }