cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

api-auth.go (6642B)


      1 // This file is part of taler-cashless2ecash.
      2 // Copyright (C) 2024 Joel Häberli
      3 //
      4 // taler-cashless2ecash is free software: you can redistribute it and/or modify it
      5 // under the terms of the GNU Affero General Public License as published
      6 // by the Free Software Foundation, either version 3 of the License,
      7 // or (at your option) any later version.
      8 //
      9 // taler-cashless2ecash is distributed in the hope that it will be useful, but
     10 // WITHOUT ANY WARRANTY; without even the implied warranty of
     11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     12 // Affero General Public License for more details.
     13 //
     14 // You should have received a copy of the GNU Affero General Public License
     15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 //
     17 // SPDX-License-Identifier: AGPL3.0-or-later
     18 
     19 package internal_api
     20 
     21 import (
     22 	internal_utils "c2ec/internal/utils"
     23 	"c2ec/pkg/config"
     24 	"c2ec/pkg/db"
     25 	"encoding/base64"
     26 	"errors"
     27 	"fmt"
     28 	"net/http"
     29 	"strconv"
     30 	"strings"
     31 )
     32 
     33 const AUTHORIZATION_HEADER = "Authorization"
     34 const BASIC_AUTH_PREFIX = "Basic "
     35 
     36 // Authenticates the Exchange against C2EC
     37 // returns true if authentication was successful, otherwise false
     38 // when not successful, the api shall return immediately
     39 // The exchange is specified to use basic auth
     40 func AuthenticateExchange(req *http.Request) bool {
     41 
     42 	auth := req.Header.Get(AUTHORIZATION_HEADER)
     43 	if basicAuth, found := strings.CutPrefix(auth, BASIC_AUTH_PREFIX); found {
     44 
     45 		ba := fmt.Sprintf("%s:%s", config.CONFIG.Server.WireGateway.Username, config.CONFIG.Server.WireGateway.Password)
     46 		encoded := base64.StdEncoding.EncodeToString([]byte(ba))
     47 		return encoded == basicAuth
     48 	}
     49 	return false
     50 }
     51 
     52 // Authenticates a terminal against C2EC
     53 // returns true if authentication was successful, otherwise false
     54 // when not successful, the api shall return immediately
     55 //
     56 // Terminals are authenticated using basic auth.
     57 // The basic authorization header MUST be base64 encoded.
     58 // The username part is the name of the provider (case sensitive) a '-' sign, followed
     59 // by the id of the terminal, which is a number.
     60 func AuthenticateTerminal(req *http.Request) bool {
     61 
     62 	auth := req.Header.Get(AUTHORIZATION_HEADER)
     63 	if basicAuth, found := strings.CutPrefix(auth, BASIC_AUTH_PREFIX); found {
     64 
     65 		decoded, err := base64.StdEncoding.DecodeString(basicAuth)
     66 		if err != nil {
     67 			internal_utils.LogWarn("auth", "failed decoding basic auth header from base64")
     68 			return false
     69 		}
     70 
     71 		username, password, err := parseBasicAuth(string(decoded))
     72 		if err != nil {
     73 			internal_utils.LogWarn("auth", "failed parsing username password from basic auth")
     74 			return false
     75 		}
     76 
     77 		provider, terminalId, err := parseTerminalUser(username)
     78 		if err != nil {
     79 			internal_utils.LogWarn("auth", "failed parsing terminal from username in basic auth")
     80 			return false
     81 		}
     82 		internal_utils.LogInfo("auth", fmt.Sprintf("req=%s by terminal with id=%d, provider=%s", req.RequestURI, terminalId, provider))
     83 
     84 		terminal, err := db.DB.GetTerminalById(terminalId)
     85 		if err != nil {
     86 			return false
     87 		}
     88 
     89 		if !terminal.Active {
     90 			internal_utils.LogWarn("auth", fmt.Sprintf("request from inactive terminal. id=%d", terminalId))
     91 			return false
     92 		}
     93 
     94 		prvdr, err := db.DB.GetTerminalProviderByName(provider)
     95 		if err != nil {
     96 			internal_utils.LogWarn("auth", fmt.Sprintf("failed requesting provider by name %s", err.Error()))
     97 			return false
     98 		}
     99 
    100 		if terminal.ProviderId != prvdr.ProviderId {
    101 			internal_utils.LogWarn("auth", "terminal's provider id did not match provider id of supplied provider")
    102 			return false
    103 		}
    104 
    105 		return internal_utils.ValidPassword(password, terminal.AccessToken)
    106 	}
    107 	internal_utils.LogWarn("auth", "basic auth prefix did not match")
    108 	return false
    109 }
    110 
    111 func AuthenticateWirewatcher(req *http.Request) bool {
    112 
    113 	auth := req.Header.Get(AUTHORIZATION_HEADER)
    114 	if basicAuth, found := strings.CutPrefix(auth, BASIC_AUTH_PREFIX); found {
    115 
    116 		decoded, err := base64.StdEncoding.DecodeString(basicAuth)
    117 		if err != nil {
    118 			internal_utils.LogWarn("auth", "failed decoding basic auth header from base64")
    119 			return false
    120 		}
    121 
    122 		username, password, err := parseBasicAuth(string(decoded))
    123 		if err != nil {
    124 			internal_utils.LogWarn("auth", "failed parsing username password from basic auth")
    125 			return false
    126 		}
    127 
    128 		if strings.EqualFold(username, config.CONFIG.Server.WireGateway.Username) &&
    129 			strings.EqualFold(password, config.CONFIG.Server.WireGateway.Password) {
    130 
    131 			return true
    132 		}
    133 	} else {
    134 		internal_utils.LogWarn("auth", "expecting exact 'Basic' prefix!")
    135 	}
    136 	internal_utils.LogWarn("auth", "basic auth prefix did not match")
    137 	return false
    138 }
    139 
    140 func parseBasicAuth(basicAuth string) (string, string, error) {
    141 
    142 	parts := strings.Split(basicAuth, ":")
    143 	if len(parts) != 2 {
    144 		return "", "", errors.New("malformed basic auth")
    145 	}
    146 	return parts[0], parts[1], nil
    147 }
    148 
    149 // parses the username of the basic auth param of the terminal.
    150 // the username has following format:
    151 //
    152 //	[PROVIDER_NAME]-[TERMINAL_ID]
    153 func parseTerminalUser(username string) (string, int, error) {
    154 
    155 	parts := strings.Split(username, "-")
    156 	if len(parts) != 2 {
    157 		return "", -1, errors.New("malformed basic auth username")
    158 	}
    159 
    160 	providerName := parts[0]
    161 	terminalId, err := strconv.Atoi(parts[1])
    162 	if err != nil {
    163 		return "", -1, errors.New("malformed basic auth username")
    164 	}
    165 
    166 	return providerName, terminalId, nil
    167 }
    168 
    169 // Parses the terminal id from the token.
    170 // This function is used to determine the terminal
    171 // which orchestrates the withdrawal.
    172 func parseTerminalId(req *http.Request) int {
    173 	auth := req.Header.Get(AUTHORIZATION_HEADER)
    174 	if basicAuth, found := strings.CutPrefix(auth, BASIC_AUTH_PREFIX); found {
    175 
    176 		decoded, err := base64.StdEncoding.DecodeString(basicAuth)
    177 		if err != nil {
    178 			return -1
    179 		}
    180 
    181 		username, _, err := parseBasicAuth(string(decoded))
    182 		if err != nil {
    183 			return -1
    184 		}
    185 
    186 		_, terminalId, err := parseTerminalUser(username)
    187 		if err != nil {
    188 			return -1
    189 		}
    190 
    191 		return terminalId
    192 	}
    193 
    194 	return -1
    195 }
    196 
    197 func parseProvider(req *http.Request) (*db.Provider, error) {
    198 
    199 	auth := req.Header.Get(AUTHORIZATION_HEADER)
    200 	if basicAuth, found := strings.CutPrefix(auth, BASIC_AUTH_PREFIX); found {
    201 
    202 		decoded, err := base64.StdEncoding.DecodeString(basicAuth)
    203 		if err != nil {
    204 			return nil, err
    205 		}
    206 
    207 		username, _, err := parseBasicAuth(string(decoded))
    208 		if err != nil {
    209 			return nil, err
    210 		}
    211 
    212 		providerName, _, err := parseTerminalUser(username)
    213 		if err != nil {
    214 			return nil, err
    215 		}
    216 
    217 		p, err := db.DB.GetTerminalProviderByName(providerName)
    218 		if err != nil {
    219 			return nil, err
    220 		}
    221 
    222 		return p, nil
    223 	}
    224 
    225 	return nil, errors.New("authorization header did not match expectations")
    226 }