{"record":{"id":"25e66409b90e10be","repo":"apereo/cas","slug":"google-authenticator-scratch-codes-encryption-key","errorCode":null,"errorMessage":"Google Authenticator scratch codes encryption key is not defined. Consider defining the encryption key to securely and safely store scratch codes.","messagePattern":"Google Authenticator scratch codes encryption key is not defined\\. Consider defining the encryption key to securely and safely store scratch codes\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"support/cas-server-support-gauth/src/main/java/org/apereo/cas/config/GoogleAuthenticatorAuthenticationEventExecutionPlanConfiguration.java","lineNumber":164,"sourceCode":"            }\n            LOGGER.warn(\"Google Authenticator one-time token account encryption/signing is turned off. \"\n                + \"Consider turning on encryption, signing to securely and safely store one-time token accounts.\");\n            return CipherExecutor.noOp();\n        }\n\n        @ConditionalOnMissingBean(name = \"googleAuthenticatorScratchCodesCipherExecutor\")\n        @Bean\n        @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)\n        public CipherExecutor googleAuthenticatorScratchCodesCipherExecutor(final ApplicationContext applicationContext,\n                                                                            final CasConfigurationProperties casProperties) {\n            return BeanSupplier.of(CipherExecutor.class)\n                .when(CONDITION_SCRATCH_CODE.given(applicationContext.getEnvironment()))\n                .supply(() -> {\n                    val key = casProperties.getAuthn().getMfa().getGauth().getCore().getScratchCodes().getEncryption().getKey();\n                    return new JasyptNumberCipherExecutor(key, \"googleAuthenticatorScratchCodesCipherExecutor\");\n                })\n                .otherwise(() -> {\n                    LOGGER.warn(\"Google Authenticator scratch codes encryption key is not defined. \"\n                        + \"Consider defining the encryption key to securely and safely store scratch codes.\");\n                    return CipherExecutor.noOp();\n                })\n                .get();\n        }\n\n        @ConditionalOnMissingBean(name = \"googlePrincipalFactory\")\n        @Bean\n        @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)\n        public PrincipalFactory googlePrincipalFactory() {\n            return PrincipalFactoryUtils.newPrincipalFactory();\n        }\n\n    }\n\n    @Configuration(value = \"GoogleAuthenticatorAuthenticationEventExecutionPlanMetadataConfiguration\", proxyBeanMethods = false)\n    @EnableConfigurationProperties(CasConfigurationProperties.class)\n    static class GoogleAuthenticatorAuthenticationEventExecutionPlanMetadataConfiguration {","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-gauth/src/main/java/org/apereo/cas/config/GoogleAuthenticatorAuthenticationEventExecutionPlanConfiguration.java#L146-L182","documentation":"When scratch-code support is enabled, CAS creates a JasyptNumberCipherExecutor to encrypt scratch codes using the key from cas.authn.mfa.gauth.core.scratch-codes.encryption.key. If that key is blank/undefined, it falls back to a no-op cipher and logs this warning, meaning scratch codes are stored unencrypted. Not a runtime failure, but a security posture warning.","triggerScenarios":"Application context startup with scratch codes condition enabled and cas.authn.mfa.gauth.core.scratch-codes.encryption.key unset or empty.","commonSituations":"Operators enable Gauth scratch codes but miss the nested scratch-codes encryption key property; keys managed only for the main gauth crypto block but not the scratch-code-specific one; config templates copied from minimal examples.","solutions":["Define cas.authn.mfa.gauth.core.scratch-codes.encryption.key with a Base64-encoded random key (e.g. openssl rand -base64 32)","Or disable scratch codes entirely if not used, so the no-op branch is irrelevant","Ensure the key is provided via environment variable/secret store in all deployment environments"],"exampleFix":"# before\ncas.authn.mfa.gauth.core.scratch-codes.encryption.key=\n# after\ncas.authn.mfa.gauth.core.scratch-codes.encryption.key=<base64-key>","handlingStrategy":"validation","validationCode":"var key = casProperties.getAuthn().getMfa().getGauth().getCore().getScratchCodes().getEncryption().getKey();\nif (key == null || key.isBlank()) {\n    throw new IllegalArgumentException(\"scratch-codes encryption key must be set\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Set the scratch-codes encryption key alongside the main gauth crypto keys","Keep scratch codes disabled unless the feature is actually used","Template CAS config with required key placeholders that fail fast when empty"],"tags":["gauth","crypto","scratch-codes","missing-key"],"backgroundTag":"missing-config-value","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}