Chris Padilla/Blog

My passion project! Posts spanning music, art, software, books, and more
You can follow by Newsletter or RSS! (What's RSS?) Full archive here.

    Credentials Authentication in Next.js

    Taking an opinionated approach, Next Auth intentionally limits the functionality available for using credentials such as email/password for logging in. The main limit is that this forces a JWT strategy instead of using a database session strategy.

    Understandably so! The number of data leaks making headlines, exposing passwords, has been a major security issue across platforms and services.

    However, the limitation takes some navigating when you are migrating from a separate backend with an existing DB and need to support older users that created accounts with the email/password method.

    Here's how I've been navigating it:

    Setup Credentials Provider

    Following the official docs will get you most of the way there. Here's the setup for my authOptions in app/api/auth/[...nextAuth]/route.js:

    import CredentialsProvider from "next-auth/providers/credentials";
    ...
    providers: [
      CredentialsProvider({
        name: 'Credentials',
        credentials: {
            username: {label: 'Username', type: 'text', placeholder: 'your-email'},
            password: {label: 'Password', type: 'password', placeholder: 'your-password'},
        },
        async authorize(credentials, req) {
            ...
        },
    }),

    Write Authorization Flow

    From here, we need to setup our authorization logic. We'll:

    1. Look up the user in the DB
    2. Verify the password
    3. Handle Matches
    4. Handle Mismatch
    async authorize(credentials, req) {
        try {
            // Add logic here to look up the user from the credentials supplied
            const foundUser = await db.collection('users').findOne({'unique.path': credentials.email});
    
            if(!foundUser) {
                // If you return null then an error will be displayed advising the user to check their details.
                return null;
                // You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
            }
    
            if(!foundUser.unique.path) {
                console.error('No password stored on user account.');
                return null;
            }
    
            const match = checkPassword(foundUser, credentials.password);
    
            if(match) {
                // Important to exclude password from return result
                delete foundUser.services;
    
                return foundUser;
            }
        } catch (e) {
            console.error(e);
        }
        return null;
    
    },

    PII

    The comments explain away most of what's going on. I'll explicitly note that here I'm using a try/catch block to handle everything. When an error occurs, the default behavior is for the error to be sent to the client and displayed. Even an incorrect password error could cause a Personally Identifiable Information (PII) error. By catching the error, we could log it with our own service and simple return null for a more generic error of "Failed login."

    Custom DB Lookup

    I'll leave explicit details out from here on how a password is verified for my use case. But, a general way you may approach this when migration:

    1. Verify with the previous framework/library the encryption method
    2. If possible, transfer over the code/libraries used
    3. Wrap it in a checkPassword() function.

    Sending Passwords over the Wire?

    A concern that came up for me: We hash passwords to the database, but is there an encryption step needed for sending it over the wire?

    Short answer: No. HTTPS covers it for the most part.

    Additionally, Next auth already takes many security steps out of the box. On their site, they list the following:

    • Designed to be secure by default and encourage best practices for safeguarding user data
    • Uses Cross-Site Request Forgery Tokens on POST routes (sign in, sign out)
    • Default cookie policy aims for the most restrictive policy appropriate for each cookie
    • When JSON Web Tokens are enabled, they are encrypted by default (JWE) with A256GCM
    • Auto-generates symmetric signing and encryption keys for developer convenience

    CSRF is the main concern here, and they have us covered!

    Integrating With Other Providers

    Next Auth also allows for using OAuth sign in as well as tokens emailed to the clients. However, it's not a straight shot. Next requires a JWT strategy, while emailing tokens requires a database strategy.

    There's some glue that needs adding from here. A post for another day!

    Fantasia!

    🦛

    I got sick the other week and watched both Fantasias with Miranda. Unbelievably beautiful!

    Automating Image Uploads to Cloudinary with Python

    There's nothing quite like the joy of automating something that you do over and over again.

    This week I wrote a python script to make my life easier with image uploads for this blog. The old routine:

    • Optimize my images locally (something Cloudinary already automates, but I do by hand for...fun?!)
    • Open up the Cloudinary portal
    • Navigate to the right directory
    • Upload the image
    • Copy the url
    • Paste the image into my markdown file
    • Optionally add optimization tag if needed

    I can eliminate most of those steps with a handy script. Here's what I whipped up, with some boilerplate provided by the Cloudinary SDK quick start guide:

    from dotenv import load_dotenv
    load_dotenv()
    
    import cloudinary
    import cloudinary.uploader
    import cloudinary.api
    import pyperclip
    
    config = cloudinary.config(secure=True)
    
    print("****1. Set up and configure the SDK:****\nCredentials: ", config.cloud_name, config.api_key, "\n")
    
    print("Image to upload:")
    input1 = input()
    input1 = input1.replace("'", "").strip()
    
    print("Where is this going? (Art default)")
    
    options = [
        "/chrisdpadilla/blog/art",
        "/chrisdpadilla/blog/images",
        "/chrisdpadilla/albums",
    ]
    
    folder = options[0]
    for i, option in enumerate(options):
        print(f'{i+1} {option}')
    
    selected_number_input = input()
    
    
    if not selected_number_input:
        selected_number_input = 1
    
    selected_number = int(selected_number_input) - 1
    if selected_number <= len(options):
        folder = options[selected_number]
    
    
    res = cloudinary.uploader.upload(input1, unique_filename = False, overwrite=True, folder=folder)
    if res.get('url', ''):
        pyperclip.copy(res['url'])
        
        print('Uploaded! Url Coppied to clipboard:')
        print(res['url'])

    Now, when I run this script in the command line, I can drag an image in, the script will ask where to save the file, and then automatically copy the url to my clipboard. Magic! ✨

    A couple of steps broken down:

    Folders

    I keep different folders for organization. Album art is in one. Blog Images in another. Art in yet another. So first, I select which one I'm looking for:

    print("Where is this going? (Art default)")
    
    options = [
        "/chrisdpadilla/blog/art",
        "/chrisdpadilla/blog/images",
        "/chrisdpadilla/albums",
    ]
    
    folder = options[0]
    for i, option in enumerate(options):
        print(f'{i+1} {option}')
    
    selected_number_input = input()

    and later on, that's passed to the cloudinary API as a folder:

    if not selected_number_input:
        selected_number_input = 1
    
    selected_number = int(selected_number_input) - 1
    if selected_number <= len(options):
        folder = options[selected_number]
    
    
    res = cloudinary.uploader.upload(input1, unique_filename = False, overwrite=True, folder=folder)

    Copying to clipboard

    Definitely the handiest, and it's just a quick install to get it. I'm using pyperclip to make it happen with this one liner:

    if res.get('url', ''):
        pyperclip.copy(res['url'])

    Clementi - Sonatina in F Maj Exposition

    Listen on Youtube

    Note to self: don't wait until a couple of weeks after practicing something to record 😅

    Blue Hair, Don't Care

    Just off to learn martial arts from a turtle guy