moodle-mod_customcert icon indicating copy to clipboard operation
moodle-mod_customcert copied to clipboard

Allow public URL for certificates

Open fulldecent opened this issue 7 months ago • 9 comments

Allow to access certificates using a secure URL.

This will allow a student to generate a URL for a certificate that they can share with anybody. Those other people do not need to have an account on the Moodle system.

A certificate that is accessible at a URL is better than a PDF file whereas the URL is self-verifying of authenticity.

Spec

  1. The URLs shall be secure and unguessable.
  2. The security shall be best practice, extensible to other projects and be namespaced.
  3. All security shall be extracted to a separate function.
  4. The entropy for the security shall be based on $CFG->siteidentifier. The security of the system shall depend on that value not being guessable.

Discuss

@mdjnelson is this issue in-scope for the project?

  • Would you like to see a separate php file for this access path (we guess this is better)? Or would you like the existing view.php to check for a valid token=... and if present then skip the user-based auth?

Implementation

I'm surprised Moodle does not have a built-in way to do this. But here is our implementation.

/**
 * Generates a secure HMAC signature for a certificate.
 *
 * This function creates a unique signature for a certificate based on its code.
 * The signature is used as a security token to verify access to the certificate.
 * It prevents unauthorized access by ensuring that only valid certificates can
 * be accessed through a generated URL.
 *
 * The signature is generated using the HMAC (Hash-based Message Authentication Code) 
 * method with SHA-256, ensuring strong security. It uses Moodle's `siteidentifier`
 * as the secret key, making it unique to each Moodle installation.
 *
 * @param string $cert_code The unique certificate code.
 * @return string The generated HMAC signature.
 */
function calculate_signature(string $cert_code): string {
    global $CFG;

    // Define a namespaced message prefix to avoid signature collisions.
    $messagePrefix = 'mod_customcert:view_user_cert';

    // Construct the message that will be signed.
    // This includes the prefix and the certificate code to create a unique hash.
    $message = $messagePrefix . '|' . $cert_code;

    // Use Moodle's unique site identifier as the secret key for HMAC.
    // This ensures that signatures are installation-specific.
    $secret = $CFG->siteidentifier;

    // Generate the HMAC hash using SHA-256.
    // This provides a cryptographic signature that is difficult to forge.
    return hash_hmac('sha256', $message, $secret);
}
/**
 * Generates a public URL for viewing a user's certificate (eCard).
 *
 * This function constructs a URL that allows public access to a certificate
 * without requiring authentication. It does so by generating a secure token
 * based on the certificate code.
 *
 * @param string $cert_code The unique code of the certificate.
 * @return string The generated public URL for the certificate.
 */
function generate_public_url_for_certificate(string $cert_code): string {
    global $CFG;

    // Generate a security token for the certificate using a private function.
    $token = calculate_signature($cert_code);

    // Construct and return the public URL to view the certificate.
    return $CFG->wwwroot . '/mod/customcert/view_user_cert.php?cert_code=' . urlencode($cert_code) . '&token=' . urlencode($token);
}

And then a separate PHP script to allow access to these files. It will verify the token in the URL and provide the file.

Thank you to @Raza403 for his help on this implementation.

fulldecent avatar Apr 29 '25 15:04 fulldecent

Could you create a webservice (see https://docs.moodle.org/500/en/Web_services) to handle generating tokens?

mdjnelson avatar May 25 '25 13:05 mdjnelson

Overall, this approach looks solid to me. I'm curious how similar functionality is handled elsewhere in Moodle - particularly around generating public URLs with tokens. I know the mobile app uses tokens, and perhaps LTI does something similar, but I’m not entirely sure. This method seems reasonable, but it would be great to know if there's an existing API or standard pattern in Moodle for this, just to ensure we're aligning with best practices.

mdjnelson avatar May 25 '25 14:05 mdjnelson

I've studied this and did not find any concept or documentation about generating these entropy URLs.

I am documenting my own best practices in Moodle-local-plugin-template.


For this PR I was thinking about achieving tokens without needing to store something in the database.

We would like to give you a web service that does like you say. But to be clear, the web service will be generating the token. As an implementation detail, it is really just calculating the token. In other words, you will be able to access the certificate with the token even if you do not call the web service first. (and to know the token you would need to know the secret Moodle config parameter.) Is that acceptable?

fulldecent avatar May 25 '25 15:05 fulldecent

@mdjnelson Does the high-entroy URL approach ^^ here work? (And a webservice to get that URL.)

fulldecent avatar May 27 '25 15:05 fulldecent

I think the direction makes sense overall, especially the use of HMAC with siteidentifier to avoid storage overhead while maintaining strong security. I appreciate the clarity around the web service just calculating the token and not needing to store anything. That seems acceptable to me.

So in summary - I’d prefer having a dedicated web service for generating tokens (even if it's just wrapping the calculation) as said earlier and a separate script to validate access using those tokens.

So yes, I'm happy for this to move forward.

Thanks!

mdjnelson avatar Jun 07 '25 14:06 mdjnelson

@mdjnelson Does the high-entroy URL approach ^^ here work? (And a webservice to get that URL.)

Ah yep, good point - I missed that you meant returning the full URL from the web service, not just the token. That sounds totally fine and probably makes things simpler for whoever's consuming it.

As long as the token logic stays secure and tidy under the hood, I'm happy either way.

So, thanks for the proposal. It would be a great addition.

I’ll take a look when ready.

Thanks!

mdjnelson avatar Jun 07 '25 14:06 mdjnelson

Thank you great discussion here. I think I can first implement this "upstream" in the best practices repo. As a minimal example.

Then I'll implement the same thing here and cite that as documentation.

Will keep you abreast of the progress

fulldecent avatar Jun 07 '25 16:06 fulldecent

@fulldecent How's progress with public API? Do you need help with it?

indicozy-eyewa avatar Aug 20 '25 06:08 indicozy-eyewa

Okay, I think the right approach here is like this:

  • [ ] Create reusable function to calculate the entropy token that can be used for any plugin in any scenario. calculate_signature(string $pluginName, string $urnSuffix) (if you know blockchain, this is like the "EIP-712" of Moodle 😄 ) this should also live in https://github.com/fulldecent/moodle-local_plugin_template with an example and documentation of this best practice
  • [ ] Create generate_public_url_for_certificate($certId) which sets the $urnSuffix and calculates the signature.
  • [ ] Create the API service which calculates returns these URLs (does not require storing anything in database when calculating URLs)
  • [ ] Create public page which validates the URL and provides the certificate

Thank you, I welcome help on the first step 😄 , getting a toy example into that other repo. (i.e. a public web page to show the last high five). Copilot may even be able to help. But we do need to test it and show there that it works, screenshots or whatever.


The rest of it, we already have that and it works. But we will refactor it to build ontop of the reusable entropy token / URN function above.

This is a new security primative, so we want to do it right and document it. And do it SOLID enough that other people can use it too :-)

fulldecent avatar Aug 22 '25 20:08 fulldecent