Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add auth-token for batch operations #356

Merged
merged 4 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions app/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ class AuthorizationError(Exception):

class ServiceUnavailableError(Exception):
pass


class AuthTokenAlreadyExistsError(Exception):
pass
13 changes: 13 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ async def send_transaction_error_handler(request: Request, exc: SendTransactionE
)


# 400:AuthTokenAlreadyExistsError
@app.exception_handler(AuthTokenAlreadyExistsError)
async def auth_token_already_exists_error_handler(request: Request, exc: AuthTokenAlreadyExistsError):
meta = {
"code": 3,
"title": "AuthTokenAlreadyExistsError"
}
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=jsonable_encoder({"meta": meta}),
)


# 400:ContractRevertError
@app.exception_handler(ContractRevertError)
async def contract_revert_error_handler(request: Request, exc: ContractRevertError):
Expand Down
1 change: 1 addition & 0 deletions app/model/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
AccountRsaKeyTemporary,
AccountRsaStatus
)
from .auth_token import AuthToken
from .token import (
Token,
TokenAttrUpdate,
Expand Down
43 changes: 43 additions & 0 deletions app/model/db/auth_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Copyright BOOSTRY Co., Ltd.

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.

SPDX-License-Identifier: Apache-2.0
"""
from datetime import datetime

from sqlalchemy import (
Column,
String,
Integer,
DateTime
)

from .base import Base


class AuthToken(Base):
"""Authentication Token"""
__tablename__ = "auth_token"

# issuer address
issuer_address = Column(String(42), primary_key=True)
# authentication token (sha256 hashed)
auth_token = Column(String(64))
# usage start
usage_start = Column(DateTime, default=datetime.utcnow)
# valid duration (sec)
# - 0: endless
valid_duration = Column(Integer, nullable=False)
4 changes: 3 additions & 1 deletion app/model/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
AccountGenerateRsaKeyRequest,
AccountChangeEOAPasswordRequest,
AccountChangeRSAPassphraseRequest,
AccountResponse
AccountAuthTokenRequest,
AccountResponse,
AccountAuthTokenResponse
)
from .e2e_messaging import (
E2EMessagingAccountCreateRequest,
Expand Down
16 changes: 15 additions & 1 deletion app/model/schema/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

SPDX-License-Identifier: Apache-2.0
"""
from datetime import datetime
from typing import Optional
from pydantic import (
BaseModel,
validator
validator,
Field
)

from config import E2EE_REQUEST_ENABLED
Expand Down Expand Up @@ -88,6 +90,11 @@ def rsa_passphrase_is_encrypted_value(cls, v):
return v


class AccountAuthTokenRequest(BaseModel):
"""Account Create Auth Token schema (REQUEST)"""
valid_duration: int = Field(None, ge=0, le=259200) # The maximum valid duration shall be 3 days.


############################
# RESPONSE
############################
Expand All @@ -98,3 +105,10 @@ class AccountResponse(BaseModel):
rsa_public_key: Optional[str]
rsa_status: AccountRsaStatus
is_deleted: bool


class AccountAuthTokenResponse(BaseModel):
"""Account Auth Token schema (RESPONSE)"""
auth_token: str
usage_start: datetime
valid_duration: int
157 changes: 147 additions & 10 deletions app/routers/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@

SPDX-License-Identifier: Apache-2.0
"""
from typing import List
import hashlib
from datetime import timedelta, datetime
from typing import List, Optional
import secrets
import re

from fastapi import (
APIRouter,
Depends
Depends,
Header,
Request
)
from fastapi.exceptions import HTTPException
from pytz import timezone
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError as SAIntegrityError
from sha3 import keccak_256
from coincurve import PublicKey
from Crypto.PublicKey import RSA
Expand All @@ -41,31 +47,48 @@
PERSONAL_INFO_RSA_PASSPHRASE_PATTERN_MSG,
E2EE_REQUEST_ENABLED,
AWS_REGION_NAME,
AWS_KMS_GENERATE_RANDOM_ENABLED
AWS_KMS_GENERATE_RANDOM_ENABLED,
TZ
)
from app.database import db_session
from app.model.schema import (
AccountCreateKeyRequest,
AccountResponse,
AccountGenerateRsaKeyRequest,
AccountChangeEOAPasswordRequest,
AccountChangeRSAPassphraseRequest
AccountChangeRSAPassphraseRequest,
AccountAuthTokenRequest,
AccountAuthTokenResponse
)
from app.utils.e2ee_utils import E2EEUtils
from app.utils.check_utils import (
validate_headers,
address_is_valid_address,
eoa_password_is_required,
eoa_password_is_encrypted_value,
check_auth
)
from app.utils.docs_utils import get_routers_responses
from app.model.db import (
Account,
AccountRsaKeyTemporary,
AccountRsaStatus,
AuthToken,
TransactionLock
)
from app.exceptions import InvalidParameterError
from app.exceptions import (
InvalidParameterError,
AuthTokenAlreadyExistsError,
AuthorizationError
)
from app import log

LOG = log.get_logger()

router = APIRouter(tags=["account"])

local_tz = timezone(TZ)


# POST: /accounts
@router.post(
Expand Down Expand Up @@ -157,7 +180,7 @@ def retrieve_account(issuer_address: str, db: Session = Depends(db_session)):
filter(Account.issuer_address == issuer_address). \
first()
if _account is None:
raise HTTPException(status_code=404, detail="issuer is not exists")
raise HTTPException(status_code=404, detail="issuer does not exist")

return {
"issuer_address": _account.issuer_address,
Expand All @@ -180,7 +203,7 @@ def delete_account(issuer_address: str, db: Session = Depends(db_session)):
filter(Account.issuer_address == issuer_address). \
first()
if _account is None:
raise HTTPException(status_code=404, detail="issuer is not exists")
raise HTTPException(status_code=404, detail="issuer does not exist")

_account.is_deleted = True
db.merge(_account)
Expand Down Expand Up @@ -211,7 +234,7 @@ def generate_rsa_key(
filter(Account.issuer_address == issuer_address). \
first()
if _account is None:
raise HTTPException(status_code=404, detail="issuer is not exists")
raise HTTPException(status_code=404, detail="issuer does not exist")

# Check now Generating RSA
if _account.rsa_status == AccountRsaStatus.CREATING.value or \
Expand Down Expand Up @@ -272,7 +295,7 @@ def change_eoa_password(
filter(Account.issuer_address == issuer_address). \
first()
if _account is None:
raise HTTPException(status_code=404, detail="issuer is not exists")
raise HTTPException(status_code=404, detail="issuer does not exist")

# Check Password Policy
eoa_password = E2EEUtils.decrypt(data.eoa_password) if E2EE_REQUEST_ENABLED else data.eoa_password
Expand Down Expand Up @@ -325,7 +348,7 @@ def change_rsa_passphrase(
filter(Account.issuer_address == issuer_address). \
first()
if _account is None:
raise HTTPException(status_code=404, detail="issuer is not exists")
raise HTTPException(status_code=404, detail="issuer does not exist")

# Check Old Passphrase
old_rsa_passphrase = E2EEUtils.decrypt(data.old_rsa_passphrase) if E2EE_REQUEST_ENABLED else data.old_rsa_passphrase
Expand All @@ -351,3 +374,117 @@ def change_rsa_passphrase(
db.commit()

return


# POST: /accounts/{issuer_address}/auth_token
@router.post(
"/accounts/{issuer_address}/auth_token",
response_model=AccountAuthTokenResponse,
responses=get_routers_responses(422, 404, InvalidParameterError, AuthTokenAlreadyExistsError))
def create_auth_token(
request: Request,
data: AccountAuthTokenRequest,
issuer_address: str,
eoa_password: Optional[str] = Header(None),
db: Session = Depends(db_session)):
"""Create Auth Token"""

# Validate Headers
validate_headers(
issuer_address=(issuer_address, address_is_valid_address),
eoa_password=(eoa_password, [eoa_password_is_required, eoa_password_is_encrypted_value])
)

# Authentication
issuer_account, _ = check_auth(
request=request,
db=db,
issuer_address=issuer_address,
eoa_password=eoa_password,
)

# Generate new auth token
new_token = secrets.token_hex()
hashed_token = hashlib.sha256(new_token.encode()).hexdigest()

# Get current datetime
current_datetime_utc = timezone("UTC").localize(datetime.utcnow())
current_datetime_local = current_datetime_utc.astimezone(local_tz).isoformat()

# Register auth token
auth_token: Optional[AuthToken] = db.query(AuthToken). \
filter(AuthToken.issuer_address == issuer_address). \
first()
if auth_token is not None:
# If a valid auth token already exists, return an error.
if auth_token.valid_duration == 0:
raise AuthTokenAlreadyExistsError()
else:
expiration_datetime = auth_token.usage_start + timedelta(seconds=auth_token.valid_duration)
if datetime.utcnow() <= expiration_datetime:
raise AuthTokenAlreadyExistsError()
Comment on lines +418 to +425
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthToken Status => Action
Not exist => Create
Exist and not expired => Error
Exist and expired => Renew

LGTM

# Update auth token
auth_token.auth_token = hashed_token
auth_token.usage_start = current_datetime_utc
auth_token.valid_duration = data.valid_duration
db.merge(auth_token)
db.commit()
else:
try:
auth_token = AuthToken()
auth_token.issuer_address = issuer_address
auth_token.auth_token = hashed_token
auth_token.usage_start = current_datetime_utc
auth_token.valid_duration = data.valid_duration
db.add(auth_token)
db.commit()
except SAIntegrityError:
# NOTE: Registration can be conflicting.
raise AuthTokenAlreadyExistsError()

return AccountAuthTokenResponse(
auth_token=new_token,
usage_start=current_datetime_local,
valid_duration=data.valid_duration
)


# DELETE: /accounts/{issuer_address}/auth_token
@router.delete(
"/accounts/{issuer_address}/auth_token",
response_model=None,
responses=get_routers_responses(422, 404, AuthorizationError)
)
def delete_auth_token(
request: Request,
issuer_address: str,
eoa_password: Optional[str] = Header(None),
auth_token: Optional[str] = Header(None),
db: Session = Depends(db_session)):
"""Delete auth token"""

# Validate Headers
validate_headers(
issuer_address=(issuer_address, address_is_valid_address),
eoa_password=(eoa_password, eoa_password_is_encrypted_value)
)

# Authentication
issuer_account, _ = check_auth(
request=request,
db=db,
issuer_address=issuer_address,
eoa_password=eoa_password,
auth_token=auth_token
)

# Delete auto token
_auth_token = db.query(AuthToken). \
filter(AuthToken.issuer_address == issuer_address). \
first()
if _auth_token is None:
raise HTTPException(status_code=404, detail="auth token does not exist")

db.delete(_auth_token)
db.commit()
return
Loading